Skip to content

Commit

Permalink
JAMES-1644 Introduce ContinuationToken
Browse files Browse the repository at this point in the history
git-svn-id: https://svn.apache.org/repos/asf/james/project/trunk@1719308 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information
mbaechler committed Dec 11, 2015
1 parent 47257ee commit bc860dd
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 9 deletions.
Expand Up @@ -19,6 +19,7 @@
package org.apache.james.jmap; package org.apache.james.jmap;


import java.io.IOException; import java.io.IOException;
import java.time.ZonedDateTime;


import javax.inject.Inject; import javax.inject.Inject;
import javax.servlet.ServletException; import javax.servlet.ServletException;
Expand All @@ -29,6 +30,7 @@
import org.apache.james.jmap.json.MultipleObjectMapperBuilder; import org.apache.james.jmap.json.MultipleObjectMapperBuilder;
import org.apache.james.jmap.model.AccessTokenRequest; import org.apache.james.jmap.model.AccessTokenRequest;
import org.apache.james.jmap.model.AccessTokenResponse; import org.apache.james.jmap.model.AccessTokenResponse;
import org.apache.james.jmap.model.ContinuationToken;
import org.apache.james.jmap.model.ContinuationTokenRequest; import org.apache.james.jmap.model.ContinuationTokenRequest;
import org.apache.james.jmap.model.ContinuationTokenResponse; import org.apache.james.jmap.model.ContinuationTokenResponse;
import org.apache.james.user.api.UsersRepository; import org.apache.james.user.api.UsersRepository;
Expand Down Expand Up @@ -106,7 +108,7 @@ private void handleContinuationTokenRequest(ContinuationTokenRequest request, Ht
ContinuationTokenResponse continuationTokenResponse = ContinuationTokenResponse ContinuationTokenResponse continuationTokenResponse = ContinuationTokenResponse
.builder() .builder()
// TODO Answer a real token // TODO Answer a real token
.continuationToken("token") .continuationToken(new ContinuationToken("fake", ZonedDateTime.now(), "fake"))
.methods(ContinuationTokenResponse.AuthenticationMethod.PASSWORD) .methods(ContinuationTokenResponse.AuthenticationMethod.PASSWORD)
.build(); .build();


Expand Down Expand Up @@ -149,4 +151,5 @@ private void returnUnauthorizedResponse(HttpServletResponse resp) throws IOExcep
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} }



} }
@@ -0,0 +1,31 @@
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/

package org.apache.james.jmap.exceptions;

public class MalformedContinuationTokenException extends Exception {

public MalformedContinuationTokenException(String s) {
super(s);
}

public MalformedContinuationTokenException(String s, Throwable throwable) {
super(s, throwable);
}
}
Expand Up @@ -20,6 +20,9 @@


import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import org.apache.james.jmap.exceptions.MalformedContinuationTokenException;

import java.time.DateTimeException;


@JsonDeserialize(builder=AccessTokenRequest.Builder.class) @JsonDeserialize(builder=AccessTokenRequest.Builder.class)
public class AccessTokenRequest { public class AccessTokenRequest {
Expand All @@ -33,14 +36,14 @@ public static Builder builder() {
@JsonPOJOBuilder(withPrefix="") @JsonPOJOBuilder(withPrefix="")
public static class Builder { public static class Builder {


private String token; private ContinuationToken token;
private String method; private String method;
private String password; private String password;


private Builder() {} private Builder() {}


public Builder token(String token) { public Builder token(String token) throws MalformedContinuationTokenException, DateTimeException {
this.token = token; this.token = ContinuationToken.fromString(token);
return this; return this;
} }


Expand All @@ -59,17 +62,17 @@ public AccessTokenRequest build() {
} }
} }


private final String token; private final ContinuationToken token;
private final String method; private final String method;
private final String password; private final String password;


private AccessTokenRequest(String token, String method, String password) { private AccessTokenRequest(ContinuationToken token, String method, String password) {
this.token = token; this.token = token;
this.method = method; this.method = method;
this.password = password; this.password = password;
} }


public String getToken() { public ContinuationToken getToken() {
return token; return token;
} }


Expand Down
@@ -0,0 +1,122 @@
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/

package org.apache.james.jmap.model;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.james.jmap.exceptions.MalformedContinuationTokenException;

import java.time.DateTimeException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

public class ContinuationToken {

public static final String SEPARATOR = "_";

private final String username;
private final ZonedDateTime expirationDate;
private final String signature;

public ContinuationToken(String username, ZonedDateTime expirationDate, String signature) {
Preconditions.checkNotNull(username);
Preconditions.checkNotNull(expirationDate);
Preconditions.checkNotNull(signature);
this.username = username;
this.expirationDate = expirationDate;
this.signature = signature;
}

public static ContinuationToken fromString(String serializedToken) throws MalformedContinuationTokenException {
Preconditions.checkArgument(!Strings.isNullOrEmpty(serializedToken), "Serialized continuation token should not be null or empty");
ImmutableList<String> tokenParts = ImmutableList.copyOf(Splitter.on(SEPARATOR).split(serializedToken));
if (tokenParts.size() < 3) {
throw new MalformedContinuationTokenException("Token " + serializedToken + " does not have enough parts");
}
Iterator<String> tokenPartsReversedIterator = tokenParts.reverse().iterator();
String signature = tokenPartsReversedIterator.next();
String expirationDateString = tokenPartsReversedIterator.next();
String username = retrieveUsername(tokenPartsReversedIterator);
try {
return new ContinuationToken(username,
ZonedDateTime.parse(expirationDateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME),
signature);
} catch(DateTimeException e) {
throw new MalformedContinuationTokenException("Token " + serializedToken + " as an incorrect date", e);
}
}

private static String retrieveUsername(Iterator<String> reversedIteratorOnUsernameParts) {
List<String> usernamePart = ImmutableList.copyOf(Lists.newArrayList(reversedIteratorOnUsernameParts)).reverse();
return Joiner.on(SEPARATOR).join(usernamePart);
}

public String getUsername() {
return username;
}

public ZonedDateTime getExpirationDate() {
return expirationDate;
}

public String getSignature() {
return signature;
}

public String serialize() {
return username
+ SEPARATOR
+ DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(expirationDate)
+ SEPARATOR
+ signature;
}

@Override
public boolean equals(Object other) {
if (other == null || getClass() != other.getClass()) {
return false;
}
ContinuationToken continuationToken = (ContinuationToken) other;
return Objects.equals(username, continuationToken.username)
&& expirationDate.isEqual(continuationToken.expirationDate)
&& Objects.equals(signature, continuationToken.signature);
}

@Override
public int hashCode() {
return Objects.hash(username, expirationDate, signature);
}

@Override
public String toString() {
return "ContinuationToken{" +
"username='" + username + '\'' +
", expirationDate=" + expirationDate +
", signature='" + signature + '\'' +
'}';
}
}
Expand Up @@ -53,8 +53,8 @@ public static class Builder {


private Builder() {} private Builder() {}


public Builder continuationToken(String continuationToken) { public Builder continuationToken(ContinuationToken continuationToken) {
this.continuationToken = continuationToken; this.continuationToken = continuationToken.serialize();
return this; return this;
} }


Expand Down
@@ -0,0 +1,97 @@
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/

package org.apache.james.jmap.model;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

import org.apache.james.jmap.exceptions.MalformedContinuationTokenException;
import org.junit.Before;
import org.junit.Test;

public class ContinuationTokenTest {

private static final String USER = "user";
private static final String EXPIRATION_DATE_STRING = "2011-12-03T10:15:30+01:00";
public static final String SIGNATURE = "signature";

private ZonedDateTime expirationDate;

@Before
public void setUp() {
expirationDate = ZonedDateTime.parse(EXPIRATION_DATE_STRING, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

@Test
public void continuationTokenShouldBeRetrievedFromString() throws Exception {
assertThat(ContinuationToken.fromString(USER + ContinuationToken.SEPARATOR + EXPIRATION_DATE_STRING + ContinuationToken.SEPARATOR + SIGNATURE))
.isEqualTo(new ContinuationToken(USER, expirationDate, SIGNATURE));
}

@Test
public void usernameShouldBeAllowedToContainSeparator() throws Exception {
String username = "user" + ContinuationToken.SEPARATOR + "using" + ContinuationToken.SEPARATOR + "separator";
assertThat(ContinuationToken.fromString(username + ContinuationToken.SEPARATOR + EXPIRATION_DATE_STRING + ContinuationToken.SEPARATOR + SIGNATURE))
.isEqualTo(new ContinuationToken(username, expirationDate, SIGNATURE));
}

@Test(expected = MalformedContinuationTokenException.class)
public void continuationTokenThatMissPartsShouldThrow() throws Exception {
ContinuationToken.fromString(EXPIRATION_DATE_STRING + ContinuationToken.SEPARATOR + SIGNATURE);
}

@Test(expected = MalformedContinuationTokenException.class)
public void continuationTokenWithMalformedDatesShouldThrow() throws Exception {
ContinuationToken.fromString(USER + ContinuationToken.SEPARATOR + "2011-25-03T10:15:30+01:00" + ContinuationToken.SEPARATOR + SIGNATURE);
}

@Test(expected = IllegalArgumentException.class)
public void nullContinuationTokenShouldThrow() throws Exception {
ContinuationToken.fromString(null);
}

@Test(expected = IllegalArgumentException.class)
public void emptyContinuationTokenShouldThrow() throws Exception {
ContinuationToken.fromString("");
}

@Test
public void getAsStringShouldReturnACorrectResult() throws Exception {
assertThat(new ContinuationToken(USER, expirationDate, SIGNATURE).serialize())
.isEqualTo(USER + ContinuationToken.SEPARATOR + EXPIRATION_DATE_STRING + ContinuationToken.SEPARATOR + SIGNATURE);
}

@Test(expected = NullPointerException.class)
public void newContinuationTokenWithNullUsernameShouldThrow() {
new ContinuationToken(null, expirationDate, SIGNATURE);
}

@Test(expected = NullPointerException.class)
public void newContinuationTokenWithNullExpirationDateShouldThrow() {
new ContinuationToken(USER, null, SIGNATURE);
}

@Test(expected = NullPointerException.class)
public void newContinuationTokenWithNullSignatureShouldThrow() {
new ContinuationToken(USER, expirationDate, null);
}
}

0 comments on commit bc860dd

Please sign in to comment.