diff --git a/Java Cookie/.classpath b/.classpath similarity index 70% rename from Java Cookie/.classpath rename to .classpath index 005c271..d8a3485 100644 --- a/Java Cookie/.classpath +++ b/.classpath @@ -1,26 +1,32 @@ - + - + - - + + - + + + + + + + - + - + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2081f21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/node +/node_modules +/bower_components \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..b77bffc --- /dev/null +++ b/.project @@ -0,0 +1,29 @@ + + + Java Cookie + + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.project.facet.core.nature + + diff --git a/Java Cookie/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs similarity index 100% rename from Java Cookie/.settings/org.eclipse.core.resources.prefs rename to .settings/org.eclipse.core.resources.prefs diff --git a/Java Cookie/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs similarity index 100% rename from Java Cookie/.settings/org.eclipse.jdt.core.prefs rename to .settings/org.eclipse.jdt.core.prefs diff --git a/Java Cookie/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs similarity index 100% rename from Java Cookie/.settings/org.eclipse.m2e.core.prefs rename to .settings/org.eclipse.m2e.core.prefs diff --git a/Java Cookie/.settings/org.eclipse.wst.common.project.facet.core.xml b/.settings/org.eclipse.wst.common.project.facet.core.xml similarity index 100% rename from Java Cookie/.settings/org.eclipse.wst.common.project.facet.core.xml rename to .settings/org.eclipse.wst.common.project.facet.core.xml diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..db99085 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,18 @@ +module.exports = function (grunt) { + + grunt.initConfig({ + bower_postinst: { + all: { + options: { + components: { + 'js-cookie': ['npm', 'grunt'] + } + } + } + } + }); + + grunt.loadNpmTasks('grunt-bower-postinst'); + + grunt.registerTask('default', ['bower_postinst']); +}; diff --git a/Java Cookie/.gitignore b/Java Cookie/.gitignore deleted file mode 100644 index ea8c4bf..0000000 --- a/Java Cookie/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/Java Cookie/pom.xml b/Java Cookie/pom.xml deleted file mode 100644 index 2d98736..0000000 --- a/Java Cookie/pom.xml +++ /dev/null @@ -1,57 +0,0 @@ - - 4.0.0 - org.jscookie - java-cookie - 0.0.1-SNAPSHOT - - UTF-8 - UTF-8 - - Java Cookie - A complete Servlet API for handling cookies - - - The MIT License (MIT) - http://opensource.org/licenses/MIT - repo - - - - https://github.com/js-cookie/java-cookie - https://github.com/js-cookie/java-cookie.git - scm:git:git@github.com:js-cookie/java-cookie.git - - - - - maven-compiler-plugin - 3.1 - - 1.7 - 1.7 - - - - org.apache.maven.plugins - maven-source-plugin - 2.4 - - - attach-sources - - jar - - - - - - - - - javax.servlet - javax.servlet-api - 3.0.1 - provided - - - \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..8ac5a2a --- /dev/null +++ b/bower.json @@ -0,0 +1,6 @@ +{ + "name": "java-cookie", + "devDependencies": { + "js-cookie": "master" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4edcf21 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "java-cookie", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/js-cookie/java-cookie.git" + }, + "devDependencies": { + "bower": "1.4.1", + "grunt": "0.4.5", + "grunt-bower-postinst": "0.2.1", + "grunt-cli": "0.1.13" + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ffb8b14 --- /dev/null +++ b/pom.xml @@ -0,0 +1,209 @@ + + 4.0.0 + org.jscookie + java-cookie + 0.0.1-SNAPSHOT + Java Cookie + A simple Servlet API for handling cookies + + UTF-8 + UTF-8 + 2.45.0 + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + https://github.com/js-cookie/java-cookie + https://github.com/js-cookie/java-cookie.git + scm:git:git@github.com:js-cookie/java-cookie.git + + + + RedHat + https://maven.repository.redhat.com/earlyaccess/all + + + + + + maven-dependency-plugin + + + unpack + process-test-classes + + unpack + + + + + org.jboss.as + jboss-as-dist + 7.5.0.Final-redhat-15 + zip + false + target + + + + + + + + org.codehaus.mojo + failsafe-maven-plugin + 2.4.3-alpha-1 + + + + integration-test + verify + + + + + + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-source-plugin + 2.4 + + + attach-sources + + jar + + + + + + com.github.eirslett + frontend-maven-plugin + 0.0.23 + + + install node and npm + + install-node-and-npm + + + v0.10.18 + 1.3.8 + + + + bower install + + bower + + + + grunt build + + grunt + + + + + + + + + + org.jboss.arquillian + arquillian-bom + 1.1.8.Final + import + pom + + + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + joda-time + joda-time + 2.7 + + + com.fasterxml.jackson.core + jackson-databind + 2.5.3 + + + org.seleniumhq.selenium + selenium-java + ${selenium.version} + test + + + org.seleniumhq.selenium + selenium-firefox-driver + ${selenium.version} + test + + + junit + junit + 4.12 + test + + + org.jboss.arquillian.junit + arquillian-junit-container + test + + + org.jboss.as + jboss-as-arquillian-container-managed + 7.2.0.Final + test + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-bom + 2.2.0-beta-2 + pom + import + + + org.mockito + mockito-all + 2.0.2-beta + test + + + + + jbossas-managed + + true + + + + org.jboss.as + jboss-as-arquillian-container-managed + 7.2.0.Final + test + + + + + \ No newline at end of file diff --git a/src/main/java/org/jscookie/AttributesDefinition.java b/src/main/java/org/jscookie/AttributesDefinition.java new file mode 100644 index 0000000..4cac229 --- /dev/null +++ b/src/main/java/org/jscookie/AttributesDefinition.java @@ -0,0 +1,14 @@ +package org.jscookie; + +public abstract class AttributesDefinition { + public abstract AttributesDefinition expires( Expiration expiration ); + abstract Expiration expires(); + public abstract AttributesDefinition path( String path ); + abstract String path(); + public abstract AttributesDefinition domain( String domain ); + abstract String domain(); + public abstract AttributesDefinition secure( Boolean secure ); + abstract Boolean secure(); + public abstract AttributesDefinition httpOnly( Boolean httpOnly ); + abstract Boolean httpOnly(); +} diff --git a/src/main/java/org/jscookie/ConverterException.java b/src/main/java/org/jscookie/ConverterException.java new file mode 100644 index 0000000..ab1be13 --- /dev/null +++ b/src/main/java/org/jscookie/ConverterException.java @@ -0,0 +1,8 @@ +package org.jscookie; + +public final class ConverterException extends Exception { + private static final long serialVersionUID = 1; + public ConverterException( Throwable cause ) { + super( cause ); + } +} diff --git a/src/main/java/org/jscookie/ConverterStrategy.java b/src/main/java/org/jscookie/ConverterStrategy.java new file mode 100644 index 0000000..bc2c7ce --- /dev/null +++ b/src/main/java/org/jscookie/ConverterStrategy.java @@ -0,0 +1,10 @@ +package org.jscookie; + +interface ConverterStrategy { + /** + * Apply the decoding strategy of a cookie. The return will be used as the cookie value + * + * @return null if the default encoding mechanism should be used instead + */ + public abstract String convert( String value, String name ) throws ConverterException; +} diff --git a/src/main/java/org/jscookie/CookieParseException.java b/src/main/java/org/jscookie/CookieParseException.java new file mode 100644 index 0000000..8d951c9 --- /dev/null +++ b/src/main/java/org/jscookie/CookieParseException.java @@ -0,0 +1,11 @@ +package org.jscookie; + +public class CookieParseException extends Exception { + private static final long serialVersionUID = 1; + CookieParseException( Throwable cause ) { + super( cause ); + } + CookieParseException( String msg ) { + super( msg ); + } +} diff --git a/src/main/java/org/jscookie/CookieSerializationException.java b/src/main/java/org/jscookie/CookieSerializationException.java new file mode 100644 index 0000000..7a5d3cb --- /dev/null +++ b/src/main/java/org/jscookie/CookieSerializationException.java @@ -0,0 +1,8 @@ +package org.jscookie; + +public class CookieSerializationException extends Exception { + private static final long serialVersionUID = 1; + CookieSerializationException( Throwable cause ) { + super( cause ); + } +} diff --git a/src/main/java/org/jscookie/CookieValue.java b/src/main/java/org/jscookie/CookieValue.java new file mode 100644 index 0000000..01c8c91 --- /dev/null +++ b/src/main/java/org/jscookie/CookieValue.java @@ -0,0 +1,7 @@ +package org.jscookie; + +/** + * Use this interface to mark that a given class can be used as a cookie value and + * serialized/deserialized to/from JSON + */ +public interface CookieValue {} diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java new file mode 100644 index 0000000..d55807f --- /dev/null +++ b/src/main/java/org/jscookie/Cookies.java @@ -0,0 +1,487 @@ +package org.jscookie; + +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +public final class Cookies implements CookiesDefinition { + private HttpServletRequest request; + private HttpServletResponse response; + private AttributesDefinition defaults = Attributes.empty(); + private ConverterStrategy converter; + private ObjectMapper mapper = new ObjectMapper(); + + private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings"; + private static ResourceBundle lStrings = ResourceBundle.getBundle( LSTRING_FILE ); + + private Cookies( HttpServletRequest request, HttpServletResponse response, ConverterStrategy converter ) { + this.request = request; + this.response = response; + this.converter = converter; + } + + public static Cookies initFromServlet( HttpServletRequest request, HttpServletResponse response ) { + return new Cookies( request, response, null ); + } + + @Override + public synchronized String get( String name ) { + if ( name == null || name.length() == 0 ) { + throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); + } + + String cookieHeader = request.getHeader( "cookie" ); + if ( cookieHeader == null ) { + return null; + } + + Map cookies = getCookies( cookieHeader ); + for ( String decodedName : cookies.keySet() ) { + if ( !name.equals( decodedName ) ) { + continue; + } + return cookies.get( decodedName ); + } + + return null; + } + + @Override + public T get( String name, Class dataType ) throws CookieParseException { + String value = get( name ); + try { + return mapper.readValue( value, dataType ); + } catch ( IOException e ) { + throw new CookieParseException( e ); + } + } + + @Override + public T get( String name, TypeReference typeRef ) throws CookieParseException { + String value = get( name ); + try { + return mapper.readValue( value, typeRef ); + } catch ( IOException e ) { + throw new CookieParseException( e ); + } + } + + @Override + public Map get() { + Map result = new HashMap(); + + String cookieHeader = request.getHeader( "cookie" ); + if ( cookieHeader == null ) { + return result; + } + + return getCookies( cookieHeader ); + } + + @Override + public synchronized void set( String name, String value, AttributesDefinition attributes ) { + if ( name == null || name.length() == 0 ) { + throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); + } + if ( value == null ) { + throw new IllegalArgumentException(); + } + if ( attributes == null ) { + throw new IllegalArgumentException(); + } + + String encodedName = encode( name ); + String encodedValue = encodeValue( value ); + + StringBuilder header = new StringBuilder(); + header.append( encodedName ); + header.append( '=' ); + header.append( encodedValue ); + + attributes = extend( Attributes.empty().path( "/" ), defaults, attributes ); + + String path = attributes.path(); + if ( path != null && !path.isEmpty() ) { + header.append( "; Path=" + path ); + } + + Expiration expires = attributes.expires(); + if ( expires != null ) { + header.append( "; Expires=" + expires.toExpiresString() ); + } + + String domain = attributes.domain(); + if ( domain != null ) { + header.append( "; Domain=" + domain ); + } + + Boolean secure = attributes.secure(); + if ( Boolean.TRUE.equals( secure ) ) { + header.append( "; Secure" ); + } + + Boolean httpOnly = attributes.httpOnly(); + if ( Boolean.TRUE.equals( httpOnly ) ) { + header.append( "; HttpOnly" ); + } + + if ( response.isCommitted() ) { + return; + } + + response.addHeader( "Set-Cookie", header.toString() ); + } + + @Override + public void set( String name, int value, AttributesDefinition attributes ) throws CookieSerializationException { + set( name, String.valueOf( value ), attributes ); + } + + @Override + public void set( String name, boolean value, AttributesDefinition attributes ) throws CookieSerializationException { + set( name, String.valueOf( value ), attributes ); + } + + @Override + public void set( String name, List value, AttributesDefinition attributes ) throws CookieSerializationException { + try { + set( name, mapper.writeValueAsString( value ), attributes ); + } catch ( JsonProcessingException e ) { + throw new CookieSerializationException( e ); + } + } + + @Override + public void set( String name, CookieValue value, AttributesDefinition attributes ) throws CookieSerializationException { + try { + set( name, mapper.writeValueAsString( value ), attributes ); + } catch ( JsonProcessingException e ) { + throw new CookieSerializationException( e ); + } + } + + @Override + public void set( String name, String value ) { + if ( name == null || name.length() == 0 ) { + throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); + } + if ( value == null ) { + throw new IllegalArgumentException(); + } + set( name, value, defaults ); + } + + @Override + public void set( String name, int value ) throws CookieSerializationException { + set( name, value, Attributes.empty() ); + } + + @Override + public void set( String name, boolean value ) { + set( name, String.valueOf( value ) ); + } + + @Override + public void set( String name, List value ) throws CookieSerializationException { + set( name, value, Attributes.empty() ); + } + + @Override + public void set( String name, CookieValue value ) throws CookieSerializationException { + set( name, value, Attributes.empty() ); + } + + @Override + public void remove( String name, AttributesDefinition attributes ) { + if ( name == null || name.length() == 0 ) { + throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); + } + if ( attributes == null ) { + throw new IllegalArgumentException(); + } + + set( name, "", extend( attributes, Attributes.empty() + .expires( Expiration.days( -1 ) )) + ); + } + + @Override + public void remove( String name ) { + if ( name == null || name.length() == 0 ) { + throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); + } + remove( name, Attributes.empty() ); + } + + @Override + public AttributesDefinition defaults() { + return this.defaults; + } + + @Override + public Cookies withConverter( ConverterStrategy converter ) { + return new Cookies( request, response, converter ); + } + + private Attributes extend( AttributesDefinition... mergeables ) { + Attributes result = Attributes.empty(); + for ( AttributesDefinition mergeable : mergeables ) { + result.merge( mergeable ); + } + return result; + } + + private String encode( String decoded ) { + return encode( decoded, new HashSet() ); + } + + private String encode( String decoded, Set exceptions ) { + String encoded = decoded; + for ( int i = 0; i < decoded.length(); ) { + int codePoint = decoded.codePointAt( i ); + i += Character.charCount( codePoint ); + + boolean isDigit = codePoint >= codePoint( "0" ) && codePoint <= codePoint( "9" ); + if ( isDigit ) { + continue; + } + + boolean isAsciiUppercaseLetter = codePoint >= codePoint( "A" ) && codePoint <= codePoint( "Z" ); + if ( isAsciiUppercaseLetter ) { + continue; + } + + boolean isAsciiLowercaseLetter = codePoint >= codePoint( "a" ) && codePoint <= codePoint( "z" ); + if ( isAsciiLowercaseLetter ) { + continue; + } + + boolean isAllowed = + codePoint == codePoint( "!" ) || codePoint == codePoint( "#" ) || + codePoint == codePoint( "$" ) || codePoint == codePoint( "&" ) || + codePoint == codePoint( "'" ) || codePoint == codePoint( "*" ) || + codePoint == codePoint( "+" ) || codePoint == codePoint( "-" ) || + codePoint == codePoint( "." ) || codePoint == codePoint( "^" ) || + codePoint == codePoint( "_" ) || codePoint == codePoint( "`" ) || + codePoint == codePoint( "|" ) || codePoint == codePoint( "~" ); + if ( isAllowed ) { + continue; + } + + if ( exceptions.contains( codePoint ) ) { + continue; + } + + try { + String character = new String( Character.toChars( codePoint ) ); + CharArrayWriter hexSequence = new CharArrayWriter(); + byte[] bytes = character.getBytes( StandardCharsets.UTF_8.name() ); + for ( int bytesIndex = 0; bytesIndex < bytes.length; bytesIndex++ ) { + char left = Character.forDigit( bytes[ bytesIndex ] >> 4 & 0xF, 16 ); + char right = Character.forDigit( bytes[ bytesIndex ] & 0xF, 16 ); + hexSequence + .append( '%' ) + .append( left ) + .append( right ); + } + String target = character.toString(); + String sequence = hexSequence.toString().toUpperCase(); + encoded = encoded.replace( target, sequence ); + } catch ( UnsupportedEncodingException e ) { + e.printStackTrace(); + } + } + return encoded; + } + + private String decode( String encoded ) { + String decoded = encoded; + Pattern pattern = Pattern.compile( "(%[0-9A-Z]{2})+" ); + Matcher matcher = pattern.matcher( encoded ); + while ( matcher.find() ) { + String encodedChar = matcher.group(); + String[] encodedBytes = encodedChar.split( "%" ); + byte[] bytes = new byte[ encodedBytes.length - 1 ]; + for ( int i = 1; i < encodedBytes.length; i++ ) { + String encodedByte = encodedBytes[ i ]; + bytes[ i - 1 ] = ( byte )Integer.parseInt( encodedByte, 16 ); + } + try { + String decodedChar = new String( bytes, StandardCharsets.UTF_8.toString() ); + decoded = decoded.replace( encodedChar, decodedChar ); + } catch ( UnsupportedEncodingException e ) { + e.printStackTrace(); + } + } + return decoded; + } + + private String encodeValue( String decodedValue ) { + Set exceptions = new HashSet<>(); + for ( int i = 0; i < decodedValue.length(); ) { + int codePoint = decodedValue.codePointAt( i ); + i += Character.charCount( codePoint ); + + boolean isIgnorable = false; + if ( codePoint == codePoint( "/" ) || codePoint == codePoint( ":") ) { + isIgnorable = true; + } + + if ( codePoint >= codePoint( "<" ) && codePoint <= codePoint( "@" ) ) { + isIgnorable = true; + } + + if ( codePoint == codePoint( "[" ) || codePoint == codePoint( "]" ) ) { + isIgnorable = true; + } + + if ( codePoint == codePoint( "{" ) || codePoint == codePoint( "}" ) ) { + isIgnorable = true; + } + + if ( isIgnorable ) { + exceptions.add( codePoint ); + } + } + + return encode( decodedValue, exceptions ); + } + + private int codePoint( String character ) { + return character.codePointAt( 0 ); + } + + private String decodeValue( String encodedValue, String decodedName ) { + String decodedValue = null; + + if ( converter != null ) { + try { + decodedValue = converter.convert( encodedValue, decodedName ); + } catch ( ConverterException e ) { + e.printStackTrace(); + } + } + + if ( decodedValue == null ) { + decodedValue = decode( encodedValue ); + } + + return decodedValue; + } + + private Map getCookies( String cookieHeader ) { + Map result = new HashMap<>(); + String[] cookies = cookieHeader.split( "; " ); + for ( int i = 0; i < cookies.length; i++ ) { + String cookie = cookies[ i ]; + String encodedName = cookie.split( "=" )[ 0 ]; + String decodedName = decode( encodedName ); + + String encodedValue = cookie.substring( cookie.indexOf( '=' ) + 1, cookie.length() ); + String decodedValue = decodeValue( encodedValue, decodedName ); + result.put( decodedName, decodedValue ); + } + return result; + } + + public static class Attributes extends AttributesDefinition { + private Expiration expires; + private String path; + private String domain; + private Boolean secure; + private Boolean httpOnly; + + private Attributes() {} + + public static Attributes empty() { + return new Attributes(); + } + + @Override + Expiration expires() { + return expires; + } + @Override + public Attributes expires( Expiration expires ) { + this.expires = expires; + return this; + } + + @Override + String path() { + return path; + } + @Override + public Attributes path( String path ) { + this.path = path; + return this; + } + + @Override + String domain() { + return domain; + } + @Override + public Attributes domain( String domain ) { + this.domain = domain; + return this; + } + + @Override + Boolean secure() { + return secure; + } + @Override + public Attributes secure( Boolean secure ) { + this.secure = secure; + return this; + } + + @Override + Boolean httpOnly() { + return httpOnly; + } + @Override + public Attributes httpOnly( Boolean httpOnly ) { + this.httpOnly = httpOnly; + return this; + } + + private Attributes merge( AttributesDefinition reference ) { + if ( reference.path() != null ) { + path = reference.path(); + } + if ( reference.domain() != null ) { + domain = reference.domain(); + } + if ( reference.expires() != null ) { + expires = reference.expires(); + } + if ( reference.secure() != null ) { + secure = reference.secure(); + } + if ( reference.httpOnly() != null ) { + httpOnly = reference.httpOnly(); + } + return this; + } + } + + public static abstract class Converter implements ConverterStrategy {} +} diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java new file mode 100644 index 0000000..46872be --- /dev/null +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -0,0 +1,151 @@ +package org.jscookie; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +interface CookiesDefinition { + /** + * Retrieves a cookie
+ * By default, assumes the characters not allowed in each cookie name are encoded with each + * one's UTF-8 Hex equivalent using percent-encoding. + * + * @return null if the cookie doesn't exist + */ + String get( String name ); + + /** + * Retrieves a cookie and parse it using the given dataType instance + * + * @throws CookieParseException + * If there's an error while parsing the cookie name using the given dataType + */ + T get( String name, Class dataType ) throws CookieParseException; + + /** + * Retrieves a cookie and parse it using the given type reference. The type reference is used + * to infer generic types from within a class and workaround Java Type Erasure.
+ * + * For more information check http://wiki.fasterxml.com/JacksonDataBinding#Full_Data_Binding + * + * @throws CookieParseException + * If there's an error while parsing the cookie name using the given dataType + * + * @see #get(String, Class) + */ + T get( String name, TypeReference typeRef ) throws CookieParseException; + + /** + * Retrieves all cookies + * + * @see #get(String) + */ + Map get(); + + /** + * Create or update an existing cookie extending the default attributes
+ * By default, the characters not allowed in the cookie name or value are encoded with each + * one's UTF-8 Hex equivalent using percent-encoding. + * + * @see #get(String) + */ + void set( String name, String value, AttributesDefinition attributes ); + + /** + * Create or update an existing cookie extending the default attributes and serializing the typed value + * + * @see #set(String, String, AttributesDefinition) + */ + void set( String name, int value, AttributesDefinition attributes ) throws CookieSerializationException; + + /** + * Create or update an existing cookie extending the default attributes and serializing the typed value + * + * @see #set(String, String, AttributesDefinition) + */ + void set( String name, boolean value, AttributesDefinition attributes ) throws CookieSerializationException; + + /** + * Create or update an existing cookie extending the default attributes and serializing the typed value + * + * @see #set(String, String, AttributesDefinition) + */ + void set( String name, List value, AttributesDefinition attributes ) throws CookieSerializationException; + + /** + * Create or update an existing cookie extending the default attributes and serializing the typed value + * + * @see #set(String, String, AttributesDefinition) + */ + void set( String name, CookieValue value, AttributesDefinition attributes ) throws CookieSerializationException; + + /** + * Create or update an existing cookie using the default attributes + * + * @see #set(String, String, AttributesDefinition) + */ + void set( String name, String value ); + + /** + * Create or update an existing cookie using the default attributes and serializing the typed value + * + * @see #set(String, String) + */ + void set( String name, int value ) throws CookieSerializationException; + + /** + * Create or update an existing cookie using the default attributes and serializing the typed value + * + * @see #set(String, String) + */ + void set( String name, boolean value ) throws CookieSerializationException; + + /** + * Create or update an existing cookie using the default attributes and serializing the typed value + * + * @see #set(String, String) + */ + void set( String name, List value ) throws CookieSerializationException; + + /** + * Create or update an existing cookie extending the default attributes and serializing the typed value + * + * @see #set(String, String) + */ + void set( String name, CookieValue value ) throws CookieSerializationException; + + /** + * Remove an existing cookie
+ * By default, assumes the characters not allowed in each cookie name are encoded with each + * one's UTF-8 Hex equivalent using percent-encoding. + * + * @param attributes + * You must pass the exact same path, domain and secure attributes that were used to set + * the cookie, unless you're relying on the default attributes + * + * @see #get(String) + */ + void remove( String name, AttributesDefinition attributes ); + + /** + * Remove an existing cookie using the default attributes + * + * @see #remove(String, AttributesDefinition) + */ + void remove( String name ); + + /** + * Retrieve the default attributes of this instance + */ + AttributesDefinition defaults(); + + /** + * Create a new instance of the api that overrides the default decoding implementation
+ * All methods that rely in a proper decoding to work, such as + * {@link #remove(String, AttributesDefinition)} and {@link #get(String)}, will run the converter first + * for each cookie.
+ * The returning String will be used as the cookie value. + */ + CookiesDefinition withConverter( ConverterStrategy converter ); +} diff --git a/src/main/java/org/jscookie/Expiration.java b/src/main/java/org/jscookie/Expiration.java new file mode 100644 index 0000000..cc6e99b --- /dev/null +++ b/src/main/java/org/jscookie/Expiration.java @@ -0,0 +1,38 @@ +package org.jscookie; + +import java.util.Date; +import java.util.Locale; + +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +public final class Expiration { + private DateTimeFormatter EXPIRES_FORMAT = DateTimeFormat + .forPattern( "EEE, dd MMM yyyy HH:mm:ss 'GMT'" ) + .withLocale( Locale.US ); + private final DateTime date; + private Expiration( DateTime dateTime ) { + this.date = dateTime; + } + public static Expiration days( int days ) { + DateTime withDays = new DateTime().plusDays( days ); + return new Expiration( withDays ); + } + public static Expiration date( DateTime dateTime ) { + if ( dateTime == null ) { + throw new IllegalArgumentException(); + } + return new Expiration( dateTime ); + } + public static Expiration date( Date date ) { + if ( date == null ) { + throw new IllegalArgumentException(); + } + DateTime dateTime = new DateTime( date ); + return new Expiration( dateTime ); + } + String toExpiresString() { + return date.toString( EXPIRES_FORMAT ); + } +} diff --git a/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java new file mode 100644 index 0000000..e39e00c --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java @@ -0,0 +1,82 @@ +package org.jscookie.test.integration.encoding; + +import java.io.File; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.Filters; +import org.jboss.shrinkwrap.api.GenericArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.importer.ExplodedImporter; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.jscookie.test.integration.qunit.QUnitPageObject; +import org.jscookie.test.integration.qunit.QUnitResults; +import org.jscookie.test.integration.test.utils.Debug; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.support.PageFactory; + +@RunWith( Arquillian.class ) +public class CookiesEncodingIT { + private static Debug debug = Debug.FALSE; + + @Deployment + public static Archive createDeployment() { + boolean RECURSIVE_TRUE = true; + + GenericArchive qunitFiles = ShrinkWrap.create( GenericArchive.class ) + .as( ExplodedImporter.class ) + .importDirectory( "bower_components/js-cookie/" ) + .as( GenericArchive.class ); + + WebArchive war = ShrinkWrap.create( WebArchive.class ) + .addPackage( "org.jscookie" ) + .addPackages( RECURSIVE_TRUE, "org.jscookie.test.integration" ) + .addAsLibraries( + Maven.resolver() + .loadPomFromFile( "pom.xml" ) + .resolve( + "joda-time:joda-time", + "com.fasterxml.jackson.core:jackson-databind" + ) + .withTransitivity() + .as( File.class ) + ) + .merge( qunitFiles, "/", Filters.includeAll() ); + + System.out.println( " ----- LOGGING THE FILES ADDED TO JBOSS" ); + System.out.println( war.toString( true ) ); + System.out.println( " ----- END OF LOGGING THE FILES ADDED TO JBOSS" ); + + return war; + } + + @RunAsClient + @Test + public void read_qunit_test( @ArquillianResource URL baseURL ) { + WebDriver driver = new FirefoxDriver(); + driver.manage().timeouts().implicitlyWait( 20, TimeUnit.SECONDS ); + + EncodingPageObject encoding = PageFactory.initElements( driver, EncodingPageObject.class ); + encoding.navigateTo( baseURL ); + QUnitPageObject qunit = encoding.qunit(); + QUnitResults results = qunit.waitForTests( debug ); + + int expectedPasses = results.getTotal(); + int actualPasses = results.getPassed(); + Assert.assertEquals( "should pass all tests", expectedPasses, actualPasses ); + + if ( debug.is( false ) ) { + driver.quit(); + } + } +} diff --git a/src/test/java/org/jscookie/test/integration/encoding/EncodingPageObject.java b/src/test/java/org/jscookie/test/integration/encoding/EncodingPageObject.java new file mode 100644 index 0000000..9dd399f --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingPageObject.java @@ -0,0 +1,23 @@ +package org.jscookie.test.integration.encoding; + +import java.net.URL; + +import org.jscookie.test.integration.qunit.QUnitPageObject; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.PageFactory; + +public class EncodingPageObject { + private WebDriver driver; + public EncodingPageObject( WebDriver driver ) { + this.driver = driver; + } + EncodingPageObject navigateTo( URL baseURL ) { + String query = "integration_baseurl=" + baseURL; + String url = baseURL + "test/encoding.html"; + driver.navigate().to( url + "?" + query ); + return this; + } + QUnitPageObject qunit() { + return PageFactory.initElements( driver, QUnitPageObject.class ); + } +} diff --git a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java new file mode 100644 index 0000000..bc7424d --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java @@ -0,0 +1,69 @@ +package org.jscookie.test.integration.encoding; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.jscookie.Cookies; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@WebServlet( "/encoding" ) +public class EncodingServlet extends HttpServlet { + private static final long serialVersionUID = 1; + @Override + public void doGet( HttpServletRequest request, HttpServletResponse response ) + throws ServletException, IOException { + String name = getUTF8Param( "name", request ); + + System.out.println( "Testing: " + name ); + + Cookies cookies = Cookies.initFromServlet( request, response ); + String value = cookies.get( name ); + + if ( value == null ) { + throw new NullPointerException( "Cookie not found with name: " + name ); + } + + cookies.set( name, value ); + + response.setContentType( "application/json" ); + new ObjectMapper() + .writeValue( response.getOutputStream(), new Result( name, value ) ); + } + + /** + * Retrieves the parameter using UTF-8 charset since the server default is ISO-8859-1 + */ + private String getUTF8Param( String name, HttpServletRequest request ) throws UnsupportedEncodingException { + String query = request.getQueryString(); + for ( String pair : query.split( "&" ) ) { + if ( name.equals( pair.split( "=" )[ 0 ] ) ) { + return URLDecoder.decode( pair.split( "=" )[ 1 ], StandardCharsets.UTF_8.name() ); + } + } + return null; + } +} + +class Result { + private String name; + private String value; + Result( String name, String value ) { + this.name = name; + this.value = value; + } + public String getName() { + return name; + } + public String getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java b/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java new file mode 100644 index 0000000..10b9c32 --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java @@ -0,0 +1,54 @@ +package org.jscookie.test.integration.qunit; + +import java.io.IOException; + +import org.jscookie.test.integration.test.utils.Debug; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.ui.WebDriverWait; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Function; + +public class QUnitPageObject { + private WebDriver driver; + private static int TIMEOUT_IN_SECONDS = 1800; + private ObjectMapper mapper = new ObjectMapper(); + public QUnitPageObject( WebDriver driver ) { + this.driver = driver; + } + public QUnitResults waitForTests( final Debug debug ) { + return new WebDriverWait( driver, TIMEOUT_IN_SECONDS ) + .until(new Function() { + @Override + public QUnitResults apply( WebDriver input ) { + JavascriptExecutor js; + if ( driver instanceof JavascriptExecutor ) { + js = ( JavascriptExecutor )driver; + String result = ( String )js.executeScript( + "return window.global_test_results && JSON.stringify(window.global_test_results)" + ); + if ( debug.is( true ) ) { + return null; + } + System.out.println( "Waiting for 'window.global_test_results': " + result ); + if ( result == null ) { + return null; + } + try { + return mapper.readValue( result, QUnitResults.class ); + } catch ( IOException cause ) { + throw new GlobalTestResultException( result, cause ); + } + } + return null; + } + }); + } + private class GlobalTestResultException extends IllegalStateException { + private static final long serialVersionUID = 1; + private GlobalTestResultException( String result, Throwable cause ) { + super( "Failed to read 'window.global_test_results': " + result, cause ); + } + } +} diff --git a/src/test/java/org/jscookie/test/integration/qunit/QUnitResults.java b/src/test/java/org/jscookie/test/integration/qunit/QUnitResults.java new file mode 100644 index 0000000..f577123 --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/qunit/QUnitResults.java @@ -0,0 +1,60 @@ +package org.jscookie.test.integration.qunit; + +import java.util.ArrayList; +import java.util.List; + +public class QUnitResults { + private int failed; + private int passed; + private int runtime; + private int total; + private List tests = new ArrayList(); + + public int getFailed() { + return failed; + } + + public int getPassed() { + return passed; + } + + public int getRuntime() { + return runtime; + } + + public List getTests() { + return tests; + } + + public int getTotal() { + return total; + } + + public static class Test { + private String actual = ""; + private String expected = ""; + private String name = ""; + private boolean result = false; + private String source; + + public String getActual() { + return actual; + } + + public String getExpected() { + return expected; + } + + public String getName() { + return name; + } + + public boolean getResult() { + return result; + } + + public String getSource() { + return source; + } + } +} diff --git a/src/test/java/org/jscookie/test/integration/test/utils/Debug.java b/src/test/java/org/jscookie/test/integration/test/utils/Debug.java new file mode 100644 index 0000000..a51067a --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/test/utils/Debug.java @@ -0,0 +1,12 @@ +package org.jscookie.test.integration.test.utils; + +public enum Debug { + TRUE( true ), FALSE( false ); + private boolean state; + private Debug( boolean state ) { + this.state = state; + } + public boolean is( boolean state ) { + return this.state == state; + } +} diff --git a/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java b/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java new file mode 100644 index 0000000..9e7d922 --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java @@ -0,0 +1,54 @@ +package org.jscookie.test.unit; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import org.jscookie.ConverterException; +import org.jscookie.Cookies; +import org.jscookie.test.unit.utils.BaseTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith( MockitoJUnitRunner.class ) +public class CookiesConverterTest extends BaseTest { + private Cookies cookies; + + @Before + public void before() { + cookies = Cookies.initFromServlet( request, response ); + } + + @Test + public void should_be_able_to_conditionally_decode_a_single_malformed_cookie() { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( + "escaped=%u5317; encoded=%E4%BA%AC" + ); + Cookies cookies = this.cookies.withConverter(new Cookies.Converter() { + @Override + public String convert( String value, String name ) throws ConverterException { + ScriptEngine javascript = new ScriptEngineManager().getEngineByName( "JavaScript" ); + if ( name.equals( "escaped" ) ) { + try { + return javascript.eval( "unescape('" + value + "')" ).toString(); + } catch ( ScriptException e ) { + throw new ConverterException( e ); + } + } + return null; + } + }); + + String actual = cookies.get( "escaped" ); + String expected = "北"; + Assert.assertEquals( expected, actual ); + + actual = cookies.get( "encoded" ); + expected = "京"; + Assert.assertEquals( expected, actual ); + } +} diff --git a/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java new file mode 100644 index 0000000..0b94f80 --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java @@ -0,0 +1,36 @@ +package org.jscookie.test.unit; + +import org.jscookie.Cookies; +import org.jscookie.test.unit.utils.BaseTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith( MockitoJUnitRunner.class ) +public class CookiesDecodingTest extends BaseTest { + private Cookies cookies; + + @Before + public void before() { + cookies = Cookies.initFromServlet( request, response ); + } + + @Test + public void character_not_allowed_in_name_and_value() { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "%3B=%3B" ); + String actual = cookies.get( ";" ); + String expected = ";"; + Assert.assertEquals( expected, actual ); + } + + @Test + public void character_with_3_bytes() { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=%E4%BA%AC" ); + String actual = cookies.get( "c" ); + String expected = "京"; + Assert.assertEquals( expected, actual ); + } +} diff --git a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java new file mode 100644 index 0000000..dbc4d92 --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -0,0 +1,67 @@ +package org.jscookie.test.unit; + +import org.jscookie.Cookies; +import org.jscookie.test.unit.utils.BaseTest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith( MockitoJUnitRunner.class ) +public class CookiesEncodingTest extends BaseTest { + private Cookies cookies; + + @Before + public void before() { + cookies = Cookies.initFromServlet( request, response ); + } + + @Test + public void character_not_allowed_in_name_and_value() { + cookies.set( ";,\\\" ", ";,\\\" " ); + Mockito.verify( response ).addHeader( "Set-Cookie", "%3B%2C%5C%22%20=%3B%2C%5C%22%20; Path=/" ); + } + + @Test + public void characters_allowed_in_name_and_value() { + cookies.set( + "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~", + "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~" + ); + Mockito.verify( response ).addHeader( + "Set-Cookie", + "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~=!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~; Path=/" + ); + } + + @Test + public void character_with_3_bytes_in_value() { + cookies.set( "c", "京" ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=%E4%BA%AC; Path=/" ); + } + + @Test + public void character_with_3_bytes_in_name() { + cookies.set( "京", "v" ); + Mockito.verify( response ).addHeader( "Set-Cookie", "%E4%BA%AC=v; Path=/" ); + } + + @Test + public void character_with_4_bytes_in_name() { + cookies.set( "c", "𩸽" ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=%F0%A9%B8%BD; Path=/" ); + } + + @Test + public void character_with_4_bytes_mixed_with_single_bytes() { + cookies.set( "c", "a𩸽b" ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=a%F0%A9%B8%BDb; Path=/" ); + } + + @Test + public void characters_allowed_in_cookie_value() { + cookies.set( "c", "/:<=>?@[]{}" ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=/:<=>?@[]{}; Path=/" ); + } +} diff --git a/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java b/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java new file mode 100644 index 0000000..771dca8 --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java @@ -0,0 +1,128 @@ +package org.jscookie.test.unit; + +import java.util.List; + +import org.jscookie.CookieParseException; +import org.jscookie.Cookies; +import org.jscookie.test.unit.utils.BaseTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; + +@RunWith( MockitoJUnitRunner.class ) +public class CookiesJSONReadTest extends BaseTest { + private Cookies cookies; + + @Before + public void before() { + cookies = Cookies.initFromServlet( request, response ); + } + + @Test + public void read_int_type() throws CookieParseException { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=1" ); + + String actual = cookies.get( "c" ); + String expected = "1"; + Assert.assertEquals( expected, actual ); + + int actual2 = cookies.get( "c", Integer.class ); + int expected2 = 1; + Assert.assertEquals( expected2, actual2 ); + } + + @Test + public void read_boolean_type() throws CookieParseException { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=true" ); + + String actual = cookies.get( "c" ); + String expected = "true"; + Assert.assertEquals( expected, actual ); + + boolean actual2 = cookies.get( "c", Boolean.class ); + boolean expected2 = true; + Assert.assertEquals( expected2, actual2 ); + } + + @Test + public void read_JSON_array_with_string() throws CookieParseException { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=[%22v%22]" ); + + String actual = cookies.get( "c" ); + String expected = "[\"v\"]"; + Assert.assertEquals( expected, actual ); + + String actual2 = cookies.get( "c", new TypeReference>() {} ).get( 0 ); + String expected2 = "v"; + Assert.assertEquals( expected2, actual2 ); + } + + @Test + public void read_custom_type_with_string_prop() throws CookieParseException { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c={%22property%22:%22v%22}" ); + + String actual = cookies.get( "c" ); + String expected = "{\"property\":\"v\"}"; + Assert.assertEquals( expected, actual ); + + String actual2 = cookies.get( "c", CustomTypeString.class ).getProperty(); + String expected2 = "v"; + Assert.assertEquals( expected2, actual2 ); + } + + @Test + public void read_custom_type_with_boolean_prop() throws CookieParseException { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c={%22property%22:true}" ); + + String actual = cookies.get( "c" ); + String expected = "{\"property\":true}"; + Assert.assertEquals( expected, actual ); + + Boolean actual2 = cookies.get( "c", CustomTypeBoolean.class ).getProperty(); + Boolean expected2 = true; + Assert.assertEquals( expected2, actual2 ); + } + + @Test + public void read_custom_type_with_number_prop() throws CookieParseException { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c={%22property%22:1}" ); + + String actual = cookies.get( "c" ); + String expected = "{\"property\":1}"; + Assert.assertEquals( expected, actual ); + + Integer actual2 = cookies.get( "c", CustomTypeInteger.class ).getProperty(); + Integer expected2 = 1; + Assert.assertEquals( expected2, actual2 ); + } + + private static class CustomTypeString { + private String property; + @JsonProperty( "property" ) + private String getProperty() { + return property; + } + } + + private static class CustomTypeBoolean { + private Boolean property; + @JsonProperty( "property" ) + private Boolean getProperty() { + return property; + } + } + + private static class CustomTypeInteger { + private Integer property; + @JsonProperty( "property" ) + private Integer getProperty() { + return property; + } + } +} diff --git a/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java new file mode 100644 index 0000000..2056a11 --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java @@ -0,0 +1,89 @@ +package org.jscookie.test.unit; + +import java.util.Arrays; + +import org.jscookie.CookieSerializationException; +import org.jscookie.CookieValue; +import org.jscookie.Cookies; +import org.jscookie.test.unit.utils.BaseTest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith( MockitoJUnitRunner.class ) +public class CookiesJSONWriteTest extends BaseTest { + private Cookies cookies; + + @Before + public void before() { + cookies = Cookies.initFromServlet( request, response ); + } + + @Test + public void write_int_type() throws CookieSerializationException { + cookies.set( "c", 1 ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=1; Path=/" ); + } + + @Test + public void write_boolean_type() throws CookieSerializationException { + cookies.set( "c", true ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=true; Path=/" ); + } + + @Test + public void write_JSON_array_with_string() throws CookieSerializationException { + cookies.set( "c", Arrays.asList( "v" ) ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=[%22v%22]; Path=/" ); + } + + @Test + public void write_custom_type_with_string_prop() throws CookieSerializationException { + cookies.set( "c", new CustomTypeString( "v" ) ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c={%22property%22:%22v%22}; Path=/" ); + } + + @Test + public void write_custom_type_with_boolean_prop() throws CookieSerializationException { + cookies.set( "c", new CustomTypeBoolean( true ) ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c={%22property%22:true}; Path=/" ); + } + + @Test + public void write_custom_type_with_number_prop() throws CookieSerializationException { + cookies.set( "c", new CustomTypeInteger( 1 ) ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c={%22property%22:1}; Path=/" ); + } + + class CustomTypeString implements CookieValue { + private String property; + private CustomTypeString( String property ) { + this.property = property; + } + public String getProperty() { + return property; + } + } + + class CustomTypeBoolean implements CookieValue { + private Boolean property; + private CustomTypeBoolean( Boolean property ) { + this.property = property; + } + public Boolean getProperty() { + return property; + } + } + + class CustomTypeInteger implements CookieValue { + private Integer property; + private CustomTypeInteger( Integer property ) { + this.property = property; + } + public Integer getProperty() { + return property; + } + } +} diff --git a/src/test/java/org/jscookie/test/unit/CookiesReadTest.java b/src/test/java/org/jscookie/test/unit/CookiesReadTest.java new file mode 100644 index 0000000..5c67c63 --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/CookiesReadTest.java @@ -0,0 +1,53 @@ +package org.jscookie.test.unit; + +import java.util.Map; + +import org.jscookie.Cookies; +import org.jscookie.test.unit.utils.BaseTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith( MockitoJUnitRunner.class ) +public class CookiesReadTest extends BaseTest { + private Cookies cookies; + + @Before + public void before() { + cookies = Cookies.initFromServlet( request, response ); + } + + @Test + public void simple_value() { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=v" ); + String actual = cookies.get( "c" ); + String expected = "v"; + Assert.assertEquals( expected, actual ); + } + + @Test + public void read_all() { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=v; foo=bar" ); + Map result = cookies.get(); + + String actual = result.get( "c" ); + String expected = "v"; + Assert.assertEquals( expected, actual ); + + actual = result.get( "foo" ); + expected = "bar"; + Assert.assertEquals( expected, actual ); + } + + @Test + public void equal_sign_in_cookie_name() { + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=a=b" ); + + String actual = cookies.get( "c" ); + String expected = "a=b"; + Assert.assertEquals( expected, actual ); + } +} diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java new file mode 100644 index 0000000..525a615 --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -0,0 +1,98 @@ +package org.jscookie.test.unit; + +import org.joda.time.DateTime; +import org.jscookie.Cookies; +import org.jscookie.Cookies.Attributes; +import org.jscookie.Expiration; +import org.jscookie.test.unit.utils.BaseTest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith( MockitoJUnitRunner.class ) +public class CookiesWriteTest extends BaseTest { + private Cookies cookies; + + @Before + public void before() { + cookies = Cookies.initFromServlet( request, response ); + } + + @Test + public void simple_write() { + cookies.set( "c", "v" ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/" ); + } + + @Test + public void simple_write_with_default_attributes() { + cookies.defaults() + .path( "/" ) + .domain( "site.com" ) + .secure( true ); + cookies.set( "c", "v" ); + + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; Domain=site.com; Secure" ); + } + + @Test + public void simple_write_with_attributes() { + cookies.set( "c", "v", Cookies.Attributes.empty() + .path( "/" ) + .domain( "example.com" ) + .secure( true ) + ); + + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; Domain=example.com; Secure" ); + } + + @Test + public void simple_write_overriding_default_attributes() { + cookies.defaults() + .path( "/path/" ) + .secure( true ); + cookies.set( "c", "v", Cookies.Attributes.empty() + .path( "/" ) + ); + + // Should consider default secure if not overriden + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; Secure" ); + } + + @Test + public void removing_default_path_should_fallback_to_whole_site() { + cookies.defaults() + .path( null ); + cookies.set( "c", "v" ); + + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/" ); + } + + @Test + public void should_not_write_the_path_attribute_if_set_as_an_empty_string() { + cookies.defaults() + .path( "" ); + cookies.set( "c", "v" ); + + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v" ); + } + + @Test + public void expires_attribute() { + DateTime date_2015_06_07_23h38m46s = new DateTime( 2015, 6, 7, 23, 38, 46 ); + cookies.set( "c", "v", Attributes.empty() + .expires( Expiration.date( date_2015_06_07_23h38m46s ) ) + ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; Expires=Sun, 07 Jun 2015 23:38:46 GMT" ); + } + + @Test + public void httponly_attribute() { + cookies.set( "c", "v", Attributes.empty() + .httpOnly( true ) + ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; HttpOnly" ); + } +} diff --git a/src/test/java/org/jscookie/test/unit/utils/BaseTest.java b/src/test/java/org/jscookie/test/unit/utils/BaseTest.java new file mode 100644 index 0000000..705111c --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/utils/BaseTest.java @@ -0,0 +1,11 @@ +package org.jscookie.test.unit.utils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.mockito.Mock; + +public class BaseTest { + protected @Mock HttpServletRequest request; + protected @Mock HttpServletResponse response; +} diff --git a/src/test/resources/arquillian.xml b/src/test/resources/arquillian.xml new file mode 100644 index 0000000..a751238 --- /dev/null +++ b/src/test/resources/arquillian.xml @@ -0,0 +1,12 @@ + + + + target/jboss-eap-6.4 + -Xdebug -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n + + +