From 45d567d4c08eaa764dd60b53df1ee74173b41f08 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 26 Apr 2015 00:55:11 -0300 Subject: [PATCH 01/63] Initial code --- Java Cookie/.classpath => .classpath | 14 +- Java Cookie/.gitignore => .gitignore | 0 .../org.eclipse.core.resources.prefs | 0 .../org.eclipse.jdt.core.prefs | 0 .../org.eclipse.m2e.core.prefs | 0 ....eclipse.wst.common.project.facet.core.xml | 0 Java Cookie/pom.xml => pom.xml | 17 +++ src/main/java/org/jscookie/Cookies.java | 134 ++++++++++++++++++ .../java/org/jscookie/CookiesHandler.java | 43 ++++++ src/main/java/org/jscookie/Expiration.java | 32 +++++ .../java/org/jscookie/CookiesReadTest.java | 31 ++++ .../java/org/jscookie/CookiesWriteTest.java | 101 +++++++++++++ .../java/org/jscookie/testutils/BaseTest.java | 19 +++ 13 files changed, 384 insertions(+), 7 deletions(-) rename Java Cookie/.classpath => .classpath (85%) rename Java Cookie/.gitignore => .gitignore (100%) rename {Java Cookie/.settings => .settings}/org.eclipse.core.resources.prefs (100%) rename {Java Cookie/.settings => .settings}/org.eclipse.jdt.core.prefs (100%) rename {Java Cookie/.settings => .settings}/org.eclipse.m2e.core.prefs (100%) rename {Java Cookie/.settings => .settings}/org.eclipse.wst.common.project.facet.core.xml (100%) rename Java Cookie/pom.xml => pom.xml (80%) create mode 100644 src/main/java/org/jscookie/Cookies.java create mode 100644 src/main/java/org/jscookie/CookiesHandler.java create mode 100644 src/main/java/org/jscookie/Expiration.java create mode 100644 src/test/java/org/jscookie/CookiesReadTest.java create mode 100644 src/test/java/org/jscookie/CookiesWriteTest.java create mode 100644 src/test/java/org/jscookie/testutils/BaseTest.java diff --git a/Java Cookie/.classpath b/.classpath similarity index 85% rename from Java Cookie/.classpath rename to .classpath index 005c271..7f30fa4 100644 --- a/Java Cookie/.classpath +++ b/.classpath @@ -1,26 +1,26 @@ - + - + - - + + - + - + - + diff --git a/Java Cookie/.gitignore b/.gitignore similarity index 100% rename from Java Cookie/.gitignore rename to .gitignore 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/Java Cookie/pom.xml b/pom.xml similarity index 80% rename from Java Cookie/pom.xml rename to pom.xml index 2d98736..e092b71 100644 --- a/Java Cookie/pom.xml +++ b/pom.xml @@ -53,5 +53,22 @@ 3.0.1 provided + + joda-time + joda-time + 2.7 + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 2.0.2-beta + test + \ No newline at end of file diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java new file mode 100644 index 0000000..7406656 --- /dev/null +++ b/src/main/java/org/jscookie/Cookies.java @@ -0,0 +1,134 @@ +package org.jscookie; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class Cookies implements CookiesHandler { + private HttpServletRequest request; + private HttpServletResponse response; + private Attributes defaults = new Attributes(); + + public Cookies( HttpServletRequest request, HttpServletResponse response ) { + this.request = request; + this.response = response; + } + + @Override + public synchronized Cookie get( String name ) { + if ( name == null ) { + throw new NullPointerException( "name is null" ); + } + // TODO handle encoding + Cookie[] cookies = request.getCookies(); + if ( cookies != null ) { + for ( Cookie cookie : cookies ) { + if ( name.equals( cookie.getName() ) ) { + return cookie; + } + } + } + return null; + } + + @Override + public synchronized void set( String name, String value, Attributes attributes ) { + // TODO handle encoding + Cookie cookie = get( name ); + + if ( cookie == null ) { + // TODO check the behavior for "org.glassfish.web.rfc2109_cookie_names_enforced" is true + cookie = new Cookie( name, value ); + } + + cookie.setValue( value ); + + attributes = extend( defaults, attributes ); + + if ( attributes.expires != null ) { + cookie.setMaxAge( attributes.expires.toSecondsFromNow() ); + } + + if ( attributes.path != null ) { + cookie.setPath( attributes.path ); + } + + if ( attributes.domain != null ) { + cookie.setDomain( attributes.domain ); + } + + if ( attributes.secure != null ) { + cookie.setSecure( attributes.secure ); + } + + response.addCookie( cookie ); + } + + @Override + public synchronized void set( String name, String value ) { + set( name, value, defaults ); + } + + @Override + public synchronized void remove( String name, Attributes attributes ) { + Cookie cookie = get( name ); + if ( cookie == null ) { + return; + } + set( name, "", extend( attributes, new Attributes() + .expires( Expiration.days( -1 ) )) + ); + } + + @Override + public synchronized void remove( String name ) { + remove( name, new Attributes() ); + } + + @Override + public void setDefaults( Attributes defaults ) { + this.defaults = defaults; + } + + private Attributes extend( Attributes a, Attributes b ) { + return new Attributes().merge( a ).merge( b ); + } + + public static class Attributes { + private Expiration expires; + private String path; + private String domain; + private Boolean secure; + public Attributes expires( Expiration expires ) { + this.expires = expires; + return this; + } + public Attributes path( String path ) { + this.path = path; + return this; + } + public Attributes domain( String domain ) { + this.domain = domain; + return this; + } + public Attributes secure( boolean secure ) { + this.secure = secure; + return this; + } + private Attributes merge( Attributes reference ) { + if ( reference.path != null ) { + path = reference.path; + } + if ( reference.domain != null ) { + domain = reference.domain; + } + if ( reference.secure != null ) { + secure = reference.secure; + } + if ( reference.expires != null ) { + expires = reference.expires; + } + return this; + } + } +} diff --git a/src/main/java/org/jscookie/CookiesHandler.java b/src/main/java/org/jscookie/CookiesHandler.java new file mode 100644 index 0000000..ee5d174 --- /dev/null +++ b/src/main/java/org/jscookie/CookiesHandler.java @@ -0,0 +1,43 @@ +package org.jscookie; + +import javax.servlet.http.Cookie; + +import org.jscookie.Cookies.Attributes; + +public interface CookiesHandler { + /** + * Retrieve a cookie + * + * @return null if the cookie doesn't exist + */ + Cookie get( String name ); + + /** + * Create or update an existing cookie overriding the default attributes + */ + void set( String name, String value, Attributes attributes ); + + /** + * Create or update an existing cookie using the default attributes + */ + void set( String name, String value ); + + /** + * Remove an existing cookie + * + * @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 + */ + void remove( String name, Attributes attributes ); + + /** + * Remove an existing cookie using the default attributes + */ + void remove( String name ); + + /** + * Change the default attributes of this instance + */ + void setDefaults( Attributes attributes ); +} diff --git a/src/main/java/org/jscookie/Expiration.java b/src/main/java/org/jscookie/Expiration.java new file mode 100644 index 0000000..d6139e8 --- /dev/null +++ b/src/main/java/org/jscookie/Expiration.java @@ -0,0 +1,32 @@ +package org.jscookie; + +import org.joda.time.DateTime; +import org.joda.time.Seconds; + +public class Expiration { + private DateTime dateTime; + private Integer days; + private Expiration( DateTime dateTime ) { + this.dateTime = dateTime; + } + private Expiration( Integer days ) { + this.days = days; + } + public static Expiration days( int days ) { + return new Expiration( days ); + } + public static Expiration date( DateTime dateTime ) { + return new Expiration( dateTime ); + } + int toSecondsFromNow() { + int seconds = 0; + if ( days != null ) { + int oneDayInSeconds = 86400; + seconds = oneDayInSeconds * days; + } else if ( dateTime != null ) { + Seconds period = Seconds.secondsBetween( DateTime.now(), dateTime ); + seconds = period.getSeconds(); + } + return seconds; + } +} diff --git a/src/test/java/org/jscookie/CookiesReadTest.java b/src/test/java/org/jscookie/CookiesReadTest.java new file mode 100644 index 0000000..13ef98e --- /dev/null +++ b/src/test/java/org/jscookie/CookiesReadTest.java @@ -0,0 +1,31 @@ +package org.jscookie; + +import javax.servlet.http.Cookie; + +import org.jscookie.testutils.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 = new Cookies( request, response ); + } + + @Test + public void simple_value() { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "c", "v" ) + }); + Cookie actual = cookies.get( "c" ); + String expected = "v"; + Assert.assertEquals( expected, actual.getValue() ); + } +} diff --git a/src/test/java/org/jscookie/CookiesWriteTest.java b/src/test/java/org/jscookie/CookiesWriteTest.java new file mode 100644 index 0000000..29f4c41 --- /dev/null +++ b/src/test/java/org/jscookie/CookiesWriteTest.java @@ -0,0 +1,101 @@ +package org.jscookie; + +import javax.servlet.http.Cookie; + +import org.jscookie.testutils.BaseTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +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 = new Cookies( request, response ); + } + + @Test + public void simple_write() { + cookies.set( "c", "v" ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "c", actual.getName() ); + Assert.assertEquals( "v", actual.getValue() ); + Assert.assertEquals( null, actual.getPath() ); + Assert.assertEquals( null, actual.getDomain() ); + Assert.assertEquals( false, actual.getSecure() ); + Assert.assertEquals( -1, actual.getMaxAge() ); + } + + @Test + public void simple_write_with_default_attributes() { + cookies.setDefaults(new Cookies.Attributes() + .path( "/" ) + .domain( "site.com" ) + .secure( true ) + .expires( Expiration.days( 1 ) ) + ); + cookies.set( "c", "v" ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "c", actual.getName() ); + Assert.assertEquals( "v", actual.getValue() ); + Assert.assertEquals( "/", actual.getPath() ); + Assert.assertEquals( "site.com", actual.getDomain() ); + Assert.assertEquals( true, actual.getSecure() ); + Assert.assertEquals( 86400, actual.getMaxAge() ); + } + + @Test + public void simple_write_with_attributes() { + cookies.set( "c", "v", new Cookies.Attributes() + .path( "/" ) + .domain( "example.com" ) + .secure( true ) + .expires( Expiration.days( 1 ) ) + ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "c", actual.getName() ); + Assert.assertEquals( "v", actual.getValue() ); + Assert.assertEquals( "/", actual.getPath() ); + Assert.assertEquals( "example.com", actual.getDomain() ); + Assert.assertEquals( true, actual.getSecure() ); + Assert.assertEquals( 86400, actual.getMaxAge() ); + } + + @Test + public void simple_write_overriding_default_attributes() { + cookies.setDefaults(new Cookies.Attributes() + .path( "/path/" ) + .secure( true ) + ); + cookies.set( "c", "v", new Cookies.Attributes() + .path( "/" ) + ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "c", actual.getName() ); + Assert.assertEquals( "v", actual.getValue() ); + Assert.assertEquals( "/", actual.getPath() ); + Assert.assertEquals( "should consider default if not overriden", true, actual.getSecure() ); + } +} diff --git a/src/test/java/org/jscookie/testutils/BaseTest.java b/src/test/java/org/jscookie/testutils/BaseTest.java new file mode 100644 index 0000000..6969b98 --- /dev/null +++ b/src/test/java/org/jscookie/testutils/BaseTest.java @@ -0,0 +1,19 @@ +package org.jscookie.testutils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.Mockito; + +public class BaseTest { + protected @Mock HttpServletRequest request; + protected @Mock HttpServletResponse response; + + @Before + public void before() { + Mockito.when( request.getCookies() ).thenReturn( new Cookie[]{} ); + } +} From cb45ad5447d67c6a45a3a8c24cf0609e19dd8677 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 26 Apr 2015 19:12:40 -0300 Subject: [PATCH 02/63] Review the code and add converters --- pom.xml | 5 + .../java/org/jscookie/ConverterException.java | 8 + src/main/java/org/jscookie/Cookies.java | 204 +++++++++++++----- .../java/org/jscookie/CookiesDefinition.java | 87 ++++++++ .../java/org/jscookie/CookiesHandler.java | 43 ---- src/main/java/org/jscookie/Expiration.java | 20 +- .../org/jscookie/CookiesConverterTest.java | 54 +++++ .../java/org/jscookie/CookiesReadTest.java | 4 +- .../java/org/jscookie/CookiesWriteTest.java | 10 +- 9 files changed, 335 insertions(+), 100 deletions(-) create mode 100644 src/main/java/org/jscookie/ConverterException.java create mode 100644 src/main/java/org/jscookie/CookiesDefinition.java delete mode 100644 src/main/java/org/jscookie/CookiesHandler.java create mode 100644 src/test/java/org/jscookie/CookiesConverterTest.java diff --git a/pom.xml b/pom.xml index e092b71..ad9d95d 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,11 @@ joda-time 2.7 + + org.eclipse.jdt + org.eclipse.jdt.annotation + 2.0.0 + junit junit diff --git a/src/main/java/org/jscookie/ConverterException.java b/src/main/java/org/jscookie/ConverterException.java new file mode 100644 index 0000000..cf817c8 --- /dev/null +++ b/src/main/java/org/jscookie/ConverterException.java @@ -0,0 +1,8 @@ +package org.jscookie; + +public class ConverterException extends Exception { + private static final long serialVersionUID = 1; + public ConverterException( Throwable cause ) { + super( cause ); + } +} diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 7406656..27d586a 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -1,13 +1,30 @@ package org.jscookie; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ResourceBundle; + import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -public class Cookies implements CookiesHandler { +import org.eclipse.jdt.annotation.Nullable; + +public class Cookies implements CookiesDefinition { private HttpServletRequest request; private HttpServletResponse response; - private Attributes defaults = new Attributes(); + private CookiesDefinition.Attributes defaults = new Attributes(); + private CookiesDefinition.Converter converter; + + private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings"; + private static ResourceBundle lStrings = ResourceBundle.getBundle( LSTRING_FILE ); + + private Cookies( HttpServletRequest request, HttpServletResponse response, CookiesDefinition.Converter converter ) { + this( request, response ); + this.converter = converter; + } public Cookies( HttpServletRequest request, HttpServletResponse response ) { this.request = request; @@ -15,120 +32,211 @@ public Cookies( HttpServletRequest request, HttpServletResponse response ) { } @Override - public synchronized Cookie get( String name ) { - if ( name == null ) { - throw new NullPointerException( "name is null" ); + public synchronized String get( String name ) { + if ( name == null || name.length() == 0 ) { + throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } - // TODO handle encoding + Cookie[] cookies = request.getCookies(); - if ( cookies != null ) { - for ( Cookie cookie : cookies ) { - if ( name.equals( cookie.getName() ) ) { - return cookie; + if ( cookies == null ) { + return null; + } + + for ( Cookie cookie : cookies ) { + String decodedValue = null; + String decodedName = null; + + try { + decodedName = URLDecoder.decode( cookie.getName(), StandardCharsets.UTF_8.name() ); + } catch ( UnsupportedEncodingException e ) { + e.printStackTrace(); + continue; + } + + if ( !name.equals( decodedName ) ) { + continue; + } + + if ( converter != null ) { + try { + decodedValue = converter.convert( cookie.getValue(), decodedName ); + } catch ( ConverterException e ) { + e.printStackTrace(); + } + } + + if ( decodedValue == null ) { + try { + decodedValue = URLDecoder.decode( cookie.getValue(), StandardCharsets.UTF_8.name() ); + } catch ( UnsupportedEncodingException e ) { + e.printStackTrace(); + continue; } } + + if ( decodedValue == null ) { + continue; + } + + return decodedValue; } + return null; } @Override - public synchronized void set( String name, String value, Attributes attributes ) { - // TODO handle encoding - Cookie cookie = get( name ); - - if ( cookie == null ) { - // TODO check the behavior for "org.glassfish.web.rfc2109_cookie_names_enforced" is true - cookie = new Cookie( name, value ); + public synchronized void set( String name, String value, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { + 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(); } - cookie.setValue( value ); + String encodedName = URLEncoder.encode( name, StandardCharsets.UTF_8.name() ); + String encodedValue = URLEncoder.encode( value, StandardCharsets.UTF_8.name() ); + Cookie cookie = new Cookie( encodedName, encodedValue ); attributes = extend( defaults, attributes ); - if ( attributes.expires != null ) { - cookie.setMaxAge( attributes.expires.toSecondsFromNow() ); + Expiration expires = attributes.expires(); + if ( expires != null ) { + cookie.setMaxAge( expires.toSecondsFromNow() ); } - if ( attributes.path != null ) { - cookie.setPath( attributes.path ); + String path = attributes.path(); + if ( path != null ) { + cookie.setPath( path ); } - if ( attributes.domain != null ) { - cookie.setDomain( attributes.domain ); + String domain = attributes.domain(); + if ( domain != null ) { + cookie.setDomain( domain ); } - if ( attributes.secure != null ) { - cookie.setSecure( attributes.secure ); + Boolean secure = attributes.secure(); + if ( secure != null ) { + cookie.setSecure( secure ); } response.addCookie( cookie ); } @Override - public synchronized void set( String name, String value ) { + public synchronized void set( String name, String value ) throws UnsupportedEncodingException { + 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 synchronized void remove( String name, Attributes attributes ) { - Cookie cookie = get( name ); - if ( cookie == null ) { - return; + public synchronized void remove( String name, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { + 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, new Attributes() .expires( Expiration.days( -1 ) )) ); } @Override - public synchronized void remove( String name ) { + public synchronized void remove( String name ) throws UnsupportedEncodingException { + if ( name == null || name.length() == 0 ) { + throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); + } remove( name, new Attributes() ); } @Override - public void setDefaults( Attributes defaults ) { + public void setDefaults( CookiesDefinition.Attributes defaults ) { + if ( defaults == null ) { + throw new IllegalArgumentException(); + } this.defaults = defaults; } - private Attributes extend( Attributes a, Attributes b ) { + @Override + public Cookies withConverter( CookiesDefinition.Converter converter ) { + return new Cookies( request, response, converter ); + } + + private Attributes extend( CookiesDefinition.Attributes a, CookiesDefinition.Attributes b ) { return new Attributes().merge( a ).merge( b ); } - public static class Attributes { + public static class Attributes extends CookiesDefinition.Attributes { private Expiration expires; private String path; private String domain; private Boolean secure; - public Attributes expires( Expiration expires ) { + + @Override + @Nullable + Expiration expires() { + return expires; + } + public Attributes expires( @Nullable Expiration expires ) { this.expires = expires; return this; } - public Attributes path( String path ) { + + @Override + @Nullable + String path() { + return path; + } + public Attributes path( @Nullable String path ) { this.path = path; return this; } - public Attributes domain( String domain ) { + + @Override + @Nullable + String domain() { + return domain; + } + public Attributes domain( @Nullable String domain ) { this.domain = domain; return this; } - public Attributes secure( boolean secure ) { + + @Override + @Nullable + Boolean secure() { + return secure; + } + public Attributes secure( @Nullable Boolean secure ) { this.secure = secure; return this; } - private Attributes merge( Attributes reference ) { - if ( reference.path != null ) { - path = reference.path; + + private Attributes merge( CookiesDefinition.Attributes reference ) { + if ( reference.path() != null ) { + path = reference.path(); } - if ( reference.domain != null ) { - domain = reference.domain; + if ( reference.domain() != null ) { + domain = reference.domain(); } - if ( reference.secure != null ) { - secure = reference.secure; + if ( reference.secure() != null ) { + secure = reference.secure(); } - if ( reference.expires != null ) { - expires = reference.expires; + if ( reference.expires() != null ) { + expires = reference.expires(); } return this; } } + + public static abstract class Converter extends CookiesDefinition.Converter {}; } diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java new file mode 100644 index 0000000..a0f33a3 --- /dev/null +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -0,0 +1,87 @@ +package org.jscookie; + +import java.io.UnsupportedEncodingException; + +public 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 ); + + /** + * 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. + * + * @throws UnsupportedEncodingException + * If the current encoding being used to encode/decode the cookie name or value is not + * supported + * + * @see #get(String) + */ + void set( String name, String value, Attributes attributes ) throws UnsupportedEncodingException; + + /** + * Create or update an existing cookie using the default attributes
+ * + * @see #set(String, String, Attributes) + */ + void set( String name, String value ) throws UnsupportedEncodingException; + + /** + * 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 + * + * @throws UnsupportedEncodingException + * If the current encoding being used to decode the cookie name is not supported + * + * @see #get(String) + */ + void remove( String name, Attributes attributes ) throws UnsupportedEncodingException; + + /** + * Remove an existing cookie using the default attributes + * + * @see #remove(String, Attributes) + */ + void remove( String name ) throws UnsupportedEncodingException; + + /** + * Change the default attributes of this instance + */ + void setDefaults( Attributes attributes ); + + /** + * 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, Attributes)} and {@link #get(String)}, will run the converter first + * for each cookie.
+ * The returning String will be used as the cookie value. + */ + CookiesDefinition withConverter( Converter converter ); + + abstract class Attributes { + abstract Expiration expires(); + abstract String path(); + abstract String domain(); + abstract Boolean secure(); + } + + abstract class Converter { + /** + * Determine 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/CookiesHandler.java b/src/main/java/org/jscookie/CookiesHandler.java deleted file mode 100644 index ee5d174..0000000 --- a/src/main/java/org/jscookie/CookiesHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.jscookie; - -import javax.servlet.http.Cookie; - -import org.jscookie.Cookies.Attributes; - -public interface CookiesHandler { - /** - * Retrieve a cookie - * - * @return null if the cookie doesn't exist - */ - Cookie get( String name ); - - /** - * Create or update an existing cookie overriding the default attributes - */ - void set( String name, String value, Attributes attributes ); - - /** - * Create or update an existing cookie using the default attributes - */ - void set( String name, String value ); - - /** - * Remove an existing cookie - * - * @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 - */ - void remove( String name, Attributes attributes ); - - /** - * Remove an existing cookie using the default attributes - */ - void remove( String name ); - - /** - * Change the default attributes of this instance - */ - void setDefaults( Attributes attributes ); -} diff --git a/src/main/java/org/jscookie/Expiration.java b/src/main/java/org/jscookie/Expiration.java index d6139e8..2adbc94 100644 --- a/src/main/java/org/jscookie/Expiration.java +++ b/src/main/java/org/jscookie/Expiration.java @@ -1,21 +1,35 @@ package org.jscookie; +import java.util.Date; + import org.joda.time.DateTime; import org.joda.time.Seconds; -public class Expiration { - private DateTime dateTime; - private Integer days; +public final class Expiration { + private final DateTime dateTime; + private final Integer days; private Expiration( DateTime dateTime ) { this.dateTime = dateTime; + this.days = null; } private Expiration( Integer days ) { + this.dateTime = null; this.days = days; } public static Expiration days( int days ) { return new Expiration( days ); } 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 ); } int toSecondsFromNow() { diff --git a/src/test/java/org/jscookie/CookiesConverterTest.java b/src/test/java/org/jscookie/CookiesConverterTest.java new file mode 100644 index 0000000..42df0df --- /dev/null +++ b/src/test/java/org/jscookie/CookiesConverterTest.java @@ -0,0 +1,54 @@ +package org.jscookie; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import javax.servlet.http.Cookie; + +import org.jscookie.testutils.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 = new Cookies( request, response ); + } + + @Test + public void should_be_able_to_conditionally_decode_a_single_malformed_cookie() { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "escaped", "%u5317" ), + new Cookie( "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/CookiesReadTest.java b/src/test/java/org/jscookie/CookiesReadTest.java index 13ef98e..d6d8f79 100644 --- a/src/test/java/org/jscookie/CookiesReadTest.java +++ b/src/test/java/org/jscookie/CookiesReadTest.java @@ -24,8 +24,8 @@ public void simple_value() { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie( "c", "v" ) }); - Cookie actual = cookies.get( "c" ); + String actual = cookies.get( "c" ); String expected = "v"; - Assert.assertEquals( expected, actual.getValue() ); + Assert.assertEquals( expected, actual ); } } diff --git a/src/test/java/org/jscookie/CookiesWriteTest.java b/src/test/java/org/jscookie/CookiesWriteTest.java index 29f4c41..ba5f3c5 100644 --- a/src/test/java/org/jscookie/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/CookiesWriteTest.java @@ -1,5 +1,7 @@ package org.jscookie; +import java.io.UnsupportedEncodingException; + import javax.servlet.http.Cookie; import org.jscookie.testutils.BaseTest; @@ -21,7 +23,7 @@ public void before() { } @Test - public void simple_write() { + public void simple_write() throws UnsupportedEncodingException { cookies.set( "c", "v" ); ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); @@ -37,7 +39,7 @@ public void simple_write() { } @Test - public void simple_write_with_default_attributes() { + public void simple_write_with_default_attributes() throws UnsupportedEncodingException { cookies.setDefaults(new Cookies.Attributes() .path( "/" ) .domain( "site.com" ) @@ -59,7 +61,7 @@ public void simple_write_with_default_attributes() { } @Test - public void simple_write_with_attributes() { + public void simple_write_with_attributes() throws UnsupportedEncodingException { cookies.set( "c", "v", new Cookies.Attributes() .path( "/" ) .domain( "example.com" ) @@ -80,7 +82,7 @@ public void simple_write_with_attributes() { } @Test - public void simple_write_overriding_default_attributes() { + public void simple_write_overriding_default_attributes() throws UnsupportedEncodingException { cookies.setDefaults(new Cookies.Attributes() .path( "/path/" ) .secure( true ) From 447d37ed851df22efc881f84fd7029a2e04ba2be Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 26 Apr 2015 19:46:50 -0300 Subject: [PATCH 03/63] Retrieve all cookies --- src/main/java/org/jscookie/Cookies.java | 83 +++++++++++++------ .../java/org/jscookie/CookiesDefinition.java | 8 ++ .../java/org/jscookie/CookiesReadTest.java | 19 +++++ 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 27d586a..83a1595 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -4,6 +4,8 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; import java.util.ResourceBundle; import javax.servlet.http.Cookie; @@ -43,45 +45,45 @@ public synchronized String get( String name ) { } for ( Cookie cookie : cookies ) { - String decodedValue = null; - String decodedName = null; - - try { - decodedName = URLDecoder.decode( cookie.getName(), StandardCharsets.UTF_8.name() ); - } catch ( UnsupportedEncodingException e ) { - e.printStackTrace(); - continue; - } + String decodedName = decodeName( cookie ); if ( !name.equals( decodedName ) ) { continue; } - if ( converter != null ) { - try { - decodedValue = converter.convert( cookie.getValue(), decodedName ); - } catch ( ConverterException e ) { - e.printStackTrace(); - } - } + String decodedValue = decodeValue( cookie, decodedName ); if ( decodedValue == null ) { - try { - decodedValue = URLDecoder.decode( cookie.getValue(), StandardCharsets.UTF_8.name() ); - } catch ( UnsupportedEncodingException e ) { - e.printStackTrace(); - continue; - } + continue; } + return decodedValue; + } + + return null; + } + + @Override + public Map get() { + Map result = new HashMap(); + + Cookie[] cookies = request.getCookies(); + if ( cookies == null ) { + return null; + } + + for ( Cookie cookie : cookies ) { + String decodedName = decodeName( cookie ); + String decodedValue = decodeValue( cookie, decodedName ); + if ( decodedValue == null ) { continue; } - return decodedValue; + result.put( decodedName, decodedValue ); } - return null; + return result; } @Override @@ -175,6 +177,37 @@ private Attributes extend( CookiesDefinition.Attributes a, CookiesDefinition.Att return new Attributes().merge( a ).merge( b ); } + private String decodeName( Cookie cookie ) { + try { + return URLDecoder.decode( cookie.getName(), StandardCharsets.UTF_8.name() ); + } catch ( UnsupportedEncodingException e ) { + e.printStackTrace(); + } + return null; + } + + private @Nullable String decodeValue( Cookie cookie, String decodedName ) { + String decodedValue = null; + + if ( converter != null ) { + try { + decodedValue = converter.convert( cookie.getValue(), decodedName ); + } catch ( ConverterException e ) { + e.printStackTrace(); + } + } + + if ( decodedValue == null ) { + try { + decodedValue = URLDecoder.decode( cookie.getValue(), StandardCharsets.UTF_8.name() ); + } catch ( UnsupportedEncodingException e ) { + e.printStackTrace(); + } + } + + return decodedValue; + } + public static class Attributes extends CookiesDefinition.Attributes { private Expiration expires; private String path; @@ -238,5 +271,5 @@ private Attributes merge( CookiesDefinition.Attributes reference ) { } } - public static abstract class Converter extends CookiesDefinition.Converter {}; + public static abstract class Converter extends CookiesDefinition.Converter {} } diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index a0f33a3..d4db0a6 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -1,6 +1,7 @@ package org.jscookie; import java.io.UnsupportedEncodingException; +import java.util.Map; public interface CookiesDefinition { /** @@ -12,6 +13,13 @@ public interface CookiesDefinition { */ String get( String name ); + /** + * 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 diff --git a/src/test/java/org/jscookie/CookiesReadTest.java b/src/test/java/org/jscookie/CookiesReadTest.java index d6d8f79..227801c 100644 --- a/src/test/java/org/jscookie/CookiesReadTest.java +++ b/src/test/java/org/jscookie/CookiesReadTest.java @@ -1,5 +1,7 @@ package org.jscookie; +import java.util.Map; + import javax.servlet.http.Cookie; import org.jscookie.testutils.BaseTest; @@ -28,4 +30,21 @@ public void simple_value() { String expected = "v"; Assert.assertEquals( expected, actual ); } + + @Test + public void read_all() { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "c", "v" ), + new Cookie( "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 ); + } } From 5b829e0de5bd197e2c3724b0fdcfdc109f0f62fa Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Mon, 27 Apr 2015 22:08:34 -0300 Subject: [PATCH 04/63] Implement parsing an Integer type in the cookie value --- src/main/java/org/jscookie/Cookies.java | 9 ++ .../java/org/jscookie/CookiesDefinition.java | 8 ++ .../java/org/jscookie/ParseException.java | 11 ++ .../org/jscookie/CookiesJSONReadTest.java | 100 ++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 src/main/java/org/jscookie/ParseException.java create mode 100644 src/test/java/org/jscookie/CookiesJSONReadTest.java diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 83a1595..32741bc 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -63,6 +63,15 @@ public synchronized String get( String name ) { return null; } + @Override + public T get( String name, Class dataType ) throws ParseException { + String value = get( name ); + if( Integer.class.equals( dataType ) ) { + return dataType.cast( Integer.parseInt( value ) ); + } + throw new ParseException( "Unsupported data type: " + dataType.getName() ); + } + @Override public Map get() { Map result = new HashMap(); diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index d4db0a6..10a5d03 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -13,6 +13,14 @@ public interface CookiesDefinition { */ String get( String name ); + /** + * Retrieves a cookie and parse it using the given dataType instance + * + * @throws ParseException + * If there's an error while parsing the cookie name using the given dataType + */ + T get( String name, Class dataType ) throws ParseException; + /** * Retrieves all cookies * diff --git a/src/main/java/org/jscookie/ParseException.java b/src/main/java/org/jscookie/ParseException.java new file mode 100644 index 0000000..25ce649 --- /dev/null +++ b/src/main/java/org/jscookie/ParseException.java @@ -0,0 +1,11 @@ +package org.jscookie; + +class ParseException extends Exception { + private static final long serialVersionUID = 1; + ParseException( Throwable cause ) { + super( cause ); + } + ParseException( String msg ) { + super( msg ); + } +} diff --git a/src/test/java/org/jscookie/CookiesJSONReadTest.java b/src/test/java/org/jscookie/CookiesJSONReadTest.java new file mode 100644 index 0000000..8a2877b --- /dev/null +++ b/src/test/java/org/jscookie/CookiesJSONReadTest.java @@ -0,0 +1,100 @@ +package org.jscookie; + +import javax.servlet.http.Cookie; + +import org.jscookie.testutils.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 CookiesJSONReadTest extends BaseTest { + private Cookies cookies; + + @Before + public void before() { + cookies = new Cookies( request, response ); + } + + @Test + public void read_int_type() throws ParseException { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "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 read_boolean_type() { +// Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { +// new Cookie( "c", "true" ) +// }); +// +// String actual = cookies.get( "c" ); +// String expected = "1"; +// Assert.assertEquals( expected, actual ); +// +// boolean actual2 = cookies.get( "c", Boolean.class ); +// boolean expected2 = true; +// Assert.assertEquals( expected2, actual2 ); +// } +// +// @Test +// public read_JSON_array() { +// Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { +// new Cookie( "c", "[\"v\"]" ) +// }); +// +// String actual = cookies.get( "c" ); +// String expected = "[\"v\"]"; +// Assert.assertEquals( expected, actual ); +// +// String actual2 = cookies.get( "c", JSONArray.class ).getString( 0 ); +// String expected2 = "v"; +// Assert.assertEquals( expected2, actual2 ); +// } +// +// @Test +// public read_custom_type() { +// Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { +// new Cookie( "c", "{\"property\":\"v\"}" ) +// }); +// +// String actual = cookies.get( "c" ); +// String expected = "{\"property\":\"v\"}"; +// Assert.assertEquals( expected, actual ); +// +// String actual2 = cookies.get( "c", CustomType.class ).getProperty(); +// String expected2 = "v"; +// Assert.assertEquals( expected2, actual2 ); +// } +// +// @Test +// public call_to_read_all_cookies_with_mixed_types() { +// Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { +// new Cookie( "c", "{\"property\":\"bar\"}" ), +// new Cookie( "c2", "v" ) +// }); +// +// JSONObject actual = cookies.get(); +// +// Assert.assertEquals( "{\"property\":\"bar\"}", actual.getString( "c" ) ); +// Assert.assertEquals( "v", actual.get( "c2" ) ); +// Assert.assertEquals( "bar", actual.get( "c", CustomType.class ).getProperty() ); +// Assert.assertEquals( "v", actual.getString( "v" ) ); +// } + +// private class CustomType { +// private String property; +// } +} From 3f4343cf3d958f3d978e5719c7e48db571f5660e Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Wed, 29 Apr 2015 20:41:10 -0300 Subject: [PATCH 05/63] parse boolean value --- src/main/java/org/jscookie/Cookies.java | 4 ++- .../org/jscookie/CookiesJSONReadTest.java | 28 +++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 32741bc..9fd0558 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -66,8 +66,10 @@ public synchronized String get( String name ) { @Override public T get( String name, Class dataType ) throws ParseException { String value = get( name ); - if( Integer.class.equals( dataType ) ) { + if ( Integer.class.equals( dataType ) ) { return dataType.cast( Integer.parseInt( value ) ); + } else if ( Boolean.class.equals( dataType ) ) { + return dataType.cast( Boolean.parseBoolean( value ) ); } throw new ParseException( "Unsupported data type: " + dataType.getName() ); } diff --git a/src/test/java/org/jscookie/CookiesJSONReadTest.java b/src/test/java/org/jscookie/CookiesJSONReadTest.java index 8a2877b..15e7f50 100644 --- a/src/test/java/org/jscookie/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/CookiesJSONReadTest.java @@ -34,20 +34,20 @@ public void read_int_type() throws ParseException { Assert.assertEquals( expected2, actual2 ); } -// @Test -// public read_boolean_type() { -// Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { -// new Cookie( "c", "true" ) -// }); -// -// String actual = cookies.get( "c" ); -// String expected = "1"; -// Assert.assertEquals( expected, actual ); -// -// boolean actual2 = cookies.get( "c", Boolean.class ); -// boolean expected2 = true; -// Assert.assertEquals( expected2, actual2 ); -// } + @Test + public void read_boolean_type() throws ParseException { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "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 read_JSON_array() { From 4b6b84b0114e477e44340113401c5ce1a0b982fc Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Wed, 29 Apr 2015 20:42:27 -0300 Subject: [PATCH 06/63] Do not extend the Cookies class unless there's an use case to do so --- src/main/java/org/jscookie/Cookies.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 9fd0558..eb1fcfd 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -14,7 +14,7 @@ import org.eclipse.jdt.annotation.Nullable; -public class Cookies implements CookiesDefinition { +public final class Cookies implements CookiesDefinition { private HttpServletRequest request; private HttpServletResponse response; private CookiesDefinition.Attributes defaults = new Attributes(); From 6e715c9b4bed672ef9009e9102dd171aa2347ae0 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Wed, 29 Apr 2015 20:42:58 -0300 Subject: [PATCH 07/63] This exception should only be used inside a converter when there's error --- src/main/java/org/jscookie/ConverterException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jscookie/ConverterException.java b/src/main/java/org/jscookie/ConverterException.java index cf817c8..ab1be13 100644 --- a/src/main/java/org/jscookie/ConverterException.java +++ b/src/main/java/org/jscookie/ConverterException.java @@ -1,6 +1,6 @@ package org.jscookie; -public class ConverterException extends Exception { +public final class ConverterException extends Exception { private static final long serialVersionUID = 1; public ConverterException( Throwable cause ) { super( cause ); From af54e06a0b1e28baf0a17357da8e8ed9bb4a75d1 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Wed, 29 Apr 2015 22:06:46 -0300 Subject: [PATCH 08/63] Use jackson data binding to parse JSON strings --- pom.xml | 5 ++ src/main/java/org/jscookie/Cookies.java | 13 ++++-- .../org/jscookie/CookiesJSONReadTest.java | 46 +++++++++++-------- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/pom.xml b/pom.xml index ad9d95d..6b9a40b 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,11 @@ org.eclipse.jdt.annotation 2.0.0
+ + com.fasterxml.jackson.core + jackson-databind + 2.5.3 + junit junit diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index eb1fcfd..ca7e610 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -1,5 +1,6 @@ package org.jscookie; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; @@ -14,11 +15,14 @@ import org.eclipse.jdt.annotation.Nullable; +import com.fasterxml.jackson.databind.ObjectMapper; + public final class Cookies implements CookiesDefinition { private HttpServletRequest request; private HttpServletResponse response; private CookiesDefinition.Attributes defaults = new Attributes(); private CookiesDefinition.Converter converter; + private ObjectMapper mapper = new ObjectMapper(); private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings"; private static ResourceBundle lStrings = ResourceBundle.getBundle( LSTRING_FILE ); @@ -66,12 +70,11 @@ public synchronized String get( String name ) { @Override public T get( String name, Class dataType ) throws ParseException { String value = get( name ); - if ( Integer.class.equals( dataType ) ) { - return dataType.cast( Integer.parseInt( value ) ); - } else if ( Boolean.class.equals( dataType ) ) { - return dataType.cast( Boolean.parseBoolean( value ) ); + try { + return mapper.readValue( value, dataType ); + } catch ( IOException e ) { + throw new ParseException( e ); } - throw new ParseException( "Unsupported data type: " + dataType.getName() ); } @Override diff --git a/src/test/java/org/jscookie/CookiesJSONReadTest.java b/src/test/java/org/jscookie/CookiesJSONReadTest.java index 15e7f50..c1e7644 100644 --- a/src/test/java/org/jscookie/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/CookiesJSONReadTest.java @@ -10,6 +10,8 @@ import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; +import com.fasterxml.jackson.annotation.JsonProperty; + @RunWith( MockitoJUnitRunner.class ) public class CookiesJSONReadTest extends BaseTest { private Cookies cookies; @@ -48,9 +50,9 @@ public void read_boolean_type() throws ParseException { boolean expected2 = true; Assert.assertEquals( expected2, actual2 ); } -// + // @Test -// public read_JSON_array() { +// public void read_JSON_array_string() { // Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { // new Cookie( "c", "[\"v\"]" ) // }); @@ -64,21 +66,21 @@ public void read_boolean_type() throws ParseException { // Assert.assertEquals( expected2, actual2 ); // } // -// @Test -// public read_custom_type() { -// Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { -// new Cookie( "c", "{\"property\":\"v\"}" ) -// }); -// -// String actual = cookies.get( "c" ); -// String expected = "{\"property\":\"v\"}"; -// Assert.assertEquals( expected, actual ); -// -// String actual2 = cookies.get( "c", CustomType.class ).getProperty(); -// String expected2 = "v"; -// Assert.assertEquals( expected2, actual2 ); -// } -// + @Test + public void read_custom_type() throws ParseException { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "c", "{\"property\":\"v\"}" ) + }); + + String actual = cookies.get( "c" ); + String expected = "{\"property\":\"v\"}"; + Assert.assertEquals( expected, actual ); + + String actual2 = cookies.get( "c", CustomType.class ).getProperty(); + String expected2 = "v"; + Assert.assertEquals( expected2, actual2 ); + } + // @Test // public call_to_read_all_cookies_with_mixed_types() { // Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { @@ -94,7 +96,11 @@ public void read_boolean_type() throws ParseException { // Assert.assertEquals( "v", actual.getString( "v" ) ); // } -// private class CustomType { -// private String property; -// } + private static class CustomType { + private String property; + @JsonProperty( "property" ) + private String getProperty() { + return property; + } + } } From 30e52016ab10c0dddf2bef0f301f6dc74553743d Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Wed, 29 Apr 2015 22:15:45 -0300 Subject: [PATCH 09/63] Create several tests for properties in custom types --- .../org/jscookie/CookiesJSONReadTest.java | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/jscookie/CookiesJSONReadTest.java b/src/test/java/org/jscookie/CookiesJSONReadTest.java index c1e7644..72f4e90 100644 --- a/src/test/java/org/jscookie/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/CookiesJSONReadTest.java @@ -67,7 +67,7 @@ public void read_boolean_type() throws ParseException { // } // @Test - public void read_custom_type() throws ParseException { + public void read_custom_type_with_string_prop() throws ParseException { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie( "c", "{\"property\":\"v\"}" ) }); @@ -76,11 +76,41 @@ public void read_custom_type() throws ParseException { String expected = "{\"property\":\"v\"}"; Assert.assertEquals( expected, actual ); - String actual2 = cookies.get( "c", CustomType.class ).getProperty(); + 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 ParseException { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "c", "{\"property\":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 ParseException { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "c", "{\"property\":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 ); + } + // @Test // public call_to_read_all_cookies_with_mixed_types() { // Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { @@ -96,11 +126,27 @@ public void read_custom_type() throws ParseException { // Assert.assertEquals( "v", actual.getString( "v" ) ); // } - private static class CustomType { + 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; + } + } } From 65992750773db29f8bda4bc86a4c0e98cc18594f Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Wed, 29 Apr 2015 22:28:17 -0300 Subject: [PATCH 10/63] Create a method to workaround Java Type Erasure --- src/main/java/org/jscookie/Cookies.java | 11 +++++++ .../java/org/jscookie/CookiesDefinition.java | 15 +++++++++ .../org/jscookie/CookiesJSONReadTest.java | 33 ++++++++++--------- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index ca7e610..06ead80 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -15,6 +15,7 @@ import org.eclipse.jdt.annotation.Nullable; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; public final class Cookies implements CookiesDefinition { @@ -77,6 +78,16 @@ public T get( String name, Class dataType ) throws ParseException { } } + @Override + public T get( String name, TypeReference typeRef ) throws ParseException { + String value = get( name ); + try { + return mapper.readValue( value, typeRef ); + } catch ( IOException e ) { + throw new ParseException( e ); + } + } + @Override public Map get() { Map result = new HashMap(); diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index 10a5d03..a8838cb 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -3,6 +3,8 @@ import java.io.UnsupportedEncodingException; import java.util.Map; +import com.fasterxml.jackson.core.type.TypeReference; + public interface CookiesDefinition { /** * Retrieves a cookie
@@ -21,6 +23,19 @@ public interface CookiesDefinition { */ T get( String name, Class dataType ) throws ParseException; + /** + * 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 ParseException + * 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 ParseException; + /** * Retrieves all cookies * diff --git a/src/test/java/org/jscookie/CookiesJSONReadTest.java b/src/test/java/org/jscookie/CookiesJSONReadTest.java index 72f4e90..e42e885 100644 --- a/src/test/java/org/jscookie/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/CookiesJSONReadTest.java @@ -1,5 +1,7 @@ package org.jscookie; +import java.util.List; + import javax.servlet.http.Cookie; import org.jscookie.testutils.BaseTest; @@ -11,6 +13,7 @@ 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 { @@ -51,21 +54,21 @@ public void read_boolean_type() throws ParseException { Assert.assertEquals( expected2, actual2 ); } -// @Test -// public void read_JSON_array_string() { -// Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { -// new Cookie( "c", "[\"v\"]" ) -// }); -// -// String actual = cookies.get( "c" ); -// String expected = "[\"v\"]"; -// Assert.assertEquals( expected, actual ); -// -// String actual2 = cookies.get( "c", JSONArray.class ).getString( 0 ); -// String expected2 = "v"; -// Assert.assertEquals( expected2, actual2 ); -// } -// + @Test + public void read_JSON_array_string() throws ParseException { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "c", "[\"v\"]" ) + }); + + 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 ParseException { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { From 93083b9d939ea524f3abcba1994011b5e473ecf5 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 30 Apr 2015 18:29:01 -0300 Subject: [PATCH 11/63] There's no need for this feature --- .../java/org/jscookie/CookiesJSONReadTest.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/test/java/org/jscookie/CookiesJSONReadTest.java b/src/test/java/org/jscookie/CookiesJSONReadTest.java index e42e885..637ffc4 100644 --- a/src/test/java/org/jscookie/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/CookiesJSONReadTest.java @@ -114,21 +114,6 @@ public void read_custom_type_with_number_prop() throws ParseException { Assert.assertEquals( expected2, actual2 ); } -// @Test -// public call_to_read_all_cookies_with_mixed_types() { -// Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { -// new Cookie( "c", "{\"property\":\"bar\"}" ), -// new Cookie( "c2", "v" ) -// }); -// -// JSONObject actual = cookies.get(); -// -// Assert.assertEquals( "{\"property\":\"bar\"}", actual.getString( "c" ) ); -// Assert.assertEquals( "v", actual.get( "c2" ) ); -// Assert.assertEquals( "bar", actual.get( "c", CustomType.class ).getProperty() ); -// Assert.assertEquals( "v", actual.getString( "v" ) ); -// } - private static class CustomTypeString { private String property; @JsonProperty( "property" ) From d3b755252c6ed3ac071a8bbd7fa10da6d2ac3fd1 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 30 Apr 2015 21:32:17 -0300 Subject: [PATCH 12/63] Create tests for JSON writing and implement int value --- src/main/java/org/jscookie/Cookies.java | 10 ++ .../java/org/jscookie/CookiesDefinition.java | 10 ++ .../org/jscookie/CookiesJSONReadTest.java | 2 +- .../org/jscookie/CookiesJSONWriteTest.java | 111 ++++++++++++++++++ 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/jscookie/CookiesJSONWriteTest.java diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 06ead80..8944a25 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -152,6 +152,11 @@ public synchronized void set( String name, String value, CookiesDefinition.Attri response.addCookie( cookie ); } + @Override + public void set( String name, int value, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { + set( name, String.valueOf( value ), attributes ); + } + @Override public synchronized void set( String name, String value ) throws UnsupportedEncodingException { if ( name == null || name.length() == 0 ) { @@ -163,6 +168,11 @@ public synchronized void set( String name, String value ) throws UnsupportedEnco set( name, value, defaults ); } + @Override + public void set( String name, int value ) throws UnsupportedEncodingException { + set( name, value, new Attributes() ); + } + @Override public synchronized void remove( String name, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { if ( name == null || name.length() == 0 ) { diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index a8838cb..efcad76 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -56,6 +56,11 @@ public interface CookiesDefinition { */ void set( String name, String value, Attributes attributes ) throws UnsupportedEncodingException; + /** + * @see #set(String, String, Attributes) + */ + void set( String name, int value, Attributes attributes ) throws UnsupportedEncodingException; + /** * Create or update an existing cookie using the default attributes
* @@ -63,6 +68,11 @@ public interface CookiesDefinition { */ void set( String name, String value ) throws UnsupportedEncodingException; + /** + * @see #set(String, int) + */ + void set( String name, int value ) throws UnsupportedEncodingException; + /** * Remove an existing cookie
* By default, assumes the characters not allowed in each cookie name are encoded with each diff --git a/src/test/java/org/jscookie/CookiesJSONReadTest.java b/src/test/java/org/jscookie/CookiesJSONReadTest.java index 637ffc4..56226bf 100644 --- a/src/test/java/org/jscookie/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/CookiesJSONReadTest.java @@ -55,7 +55,7 @@ public void read_boolean_type() throws ParseException { } @Test - public void read_JSON_array_string() throws ParseException { + public void read_JSON_array_with_string() throws ParseException { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie( "c", "[\"v\"]" ) }); diff --git a/src/test/java/org/jscookie/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/CookiesJSONWriteTest.java new file mode 100644 index 0000000..1077f3a --- /dev/null +++ b/src/test/java/org/jscookie/CookiesJSONWriteTest.java @@ -0,0 +1,111 @@ +package org.jscookie; + +import java.io.UnsupportedEncodingException; + +import javax.servlet.http.Cookie; + +import org.jscookie.testutils.BaseTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +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 = new Cookies( request, response ); + } + + @Test + public void write_int_type() throws UnsupportedEncodingException { + cookies.set( "c", 1 ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "1", actual.getValue() ); + } + +// @Test +// public void write_boolean_type() { +// cookies.set( "c", true ); +// +// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); +// Mockito.verify( response ).addCookie( argument.capture() ); +// +// Cookie actual = argument.getValue(); +// Assert.assertEquals( "true", actual.getValue() ); +// } +// +// @Test +// public void write_JSON_array_with_string() { +// cookies.set( "c", Arrays.asList( "v" ) ); +// +// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); +// Mockito.verify( response ).addCookie( argument.capture() ); +// +// Cookie actual = argument.getValue(); +// Assert.assertEquals( "v", actual.getValue() ); +// } +// +// @Test +// public void write_custom_type_with_string_prop() { +// cookies.set( "c", new CustomTypeString( "v" ) ); +// +// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); +// Mockito.verify( response ).addCookie( argument.capture() ); +// +// Cookie actual = argument.getValue(); +// Assert.assertEquals( "v", actual.getValue() ); +// } +// +// @Test +// public void write_custom_type_with_boolean_prop() { +// cookies.set( "c", new CustomTypeBoolean( true ) ); +// +// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); +// Mockito.verify( response ).addCookie( argument.capture() ); +// +// Cookie actual = argument.getValue(); +// Assert.assertEquals( "true", actual.getValue() ); +// } +// +// @Test +// public void write_custom_type_with_number_prop() { +// cookies.set( "c", new CustomTypeInteger( 1 ) ); +// +// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); +// Mockito.verify( response ).addCookie( argument.capture() ); +// +// Cookie actual = argument.getValue(); +// Assert.assertEquals( "1", actual.getValue() ); +// } +// +// private class CustomTypeString { +// private String property; +// private CustomTypeString( String property ) { +// this.property = property; +// } +// } +// +// private class CustomTypeBoolean { +// private Boolean property; +// private CustomTypeBoolean( Boolean property ) { +// this.property = property; +// } +// } +// +// private class CustomTypeInteger { +// private Integer property; +// private CustomTypeInteger( Integer property ) { +// this.property = property; +// } +// } +} From 9f7473f327706191fe2e129947018ef25aabec2c Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 30 Apr 2015 21:38:43 -0300 Subject: [PATCH 13/63] Make the docs appear in eclipse. Need to have content not just the "see" --- src/main/java/org/jscookie/CookiesDefinition.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index efcad76..5cdeff1 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -57,18 +57,22 @@ public interface CookiesDefinition { void set( String name, String value, Attributes attributes ) throws UnsupportedEncodingException; /** + * Create or update an existing cookie extending the default attributes and serializing the typed value + * * @see #set(String, String, Attributes) */ void set( String name, int value, Attributes attributes ) throws UnsupportedEncodingException; /** - * Create or update an existing cookie using the default attributes
+ * Create or update an existing cookie using the default attributes * * @see #set(String, String, Attributes) */ void set( String name, String value ) throws UnsupportedEncodingException; /** + * Create or update an existing cookie using the default attributes and serializing the typed value + * * @see #set(String, int) */ void set( String name, int value ) throws UnsupportedEncodingException; From e9ea285590773c0c27cfc34beebba378d6d08860 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 30 Apr 2015 21:53:15 -0300 Subject: [PATCH 14/63] Implement boolean writing --- src/main/java/org/jscookie/Cookies.java | 10 +++++++++ .../java/org/jscookie/CookiesDefinition.java | 16 +++++++++++++- .../org/jscookie/CookiesJSONWriteTest.java | 22 +++++++++---------- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 8944a25..90b0798 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -157,6 +157,11 @@ public void set( String name, int value, CookiesDefinition.Attributes attributes set( name, String.valueOf( value ), attributes ); } + @Override + public void set( String name, boolean value, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { + set( name, String.valueOf( value ), attributes ); + } + @Override public synchronized void set( String name, String value ) throws UnsupportedEncodingException { if ( name == null || name.length() == 0 ) { @@ -173,6 +178,11 @@ public void set( String name, int value ) throws UnsupportedEncodingException { set( name, value, new Attributes() ); } + @Override + public void set( String name, boolean value ) throws UnsupportedEncodingException { + set( name, String.valueOf( value ) ); + } + @Override public synchronized void remove( String name, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { if ( name == null || name.length() == 0 ) { diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index 5cdeff1..514a840 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -63,6 +63,13 @@ public interface CookiesDefinition { */ void set( String name, int value, Attributes attributes ) throws UnsupportedEncodingException; + /** + * Create or update an existing cookie extending the default attributes and serializing the typed value + * + * @see #set(String, String, Attributes) + */ + void set( String name, boolean value, Attributes attributes ) throws UnsupportedEncodingException; + /** * Create or update an existing cookie using the default attributes * @@ -73,10 +80,17 @@ public interface CookiesDefinition { /** * Create or update an existing cookie using the default attributes and serializing the typed value * - * @see #set(String, int) + * @see #set(String, String) */ void set( String name, int value ) throws UnsupportedEncodingException; + /** + * 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 UnsupportedEncodingException; + /** * Remove an existing cookie
* By default, assumes the characters not allowed in each cookie name are encoded with each diff --git a/src/test/java/org/jscookie/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/CookiesJSONWriteTest.java index 1077f3a..3d4743f 100644 --- a/src/test/java/org/jscookie/CookiesJSONWriteTest.java +++ b/src/test/java/org/jscookie/CookiesJSONWriteTest.java @@ -33,17 +33,17 @@ public void write_int_type() throws UnsupportedEncodingException { Assert.assertEquals( "1", actual.getValue() ); } -// @Test -// public void write_boolean_type() { -// cookies.set( "c", true ); -// -// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); -// Mockito.verify( response ).addCookie( argument.capture() ); -// -// Cookie actual = argument.getValue(); -// Assert.assertEquals( "true", actual.getValue() ); -// } -// + @Test + public void write_boolean_type() throws UnsupportedEncodingException { + cookies.set( "c", true ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "true", actual.getValue() ); + } + // @Test // public void write_JSON_array_with_string() { // cookies.set( "c", Arrays.asList( "v" ) ); From 3c883a752985d346b2ae466e91fbe49d39907770 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 30 Apr 2015 22:50:39 -0300 Subject: [PATCH 15/63] Implement JSONArray write. Use Jackson for all other types. --- .../org/jscookie/CookieParseException.java | 11 ++++ .../CookieSerializationException.java | 8 +++ src/main/java/org/jscookie/Cookies.java | 50 +++++++++++++++---- .../java/org/jscookie/CookiesDefinition.java | 31 +++++++++--- .../java/org/jscookie/ParseException.java | 11 ---- .../org/jscookie/CookiesJSONReadTest.java | 12 ++--- .../org/jscookie/CookiesJSONWriteTest.java | 28 +++++------ 7 files changed, 101 insertions(+), 50 deletions(-) create mode 100644 src/main/java/org/jscookie/CookieParseException.java create mode 100644 src/main/java/org/jscookie/CookieSerializationException.java delete mode 100644 src/main/java/org/jscookie/ParseException.java diff --git a/src/main/java/org/jscookie/CookieParseException.java b/src/main/java/org/jscookie/CookieParseException.java new file mode 100644 index 0000000..685fdbb --- /dev/null +++ b/src/main/java/org/jscookie/CookieParseException.java @@ -0,0 +1,11 @@ +package org.jscookie; + +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..b8bc1fe --- /dev/null +++ b/src/main/java/org/jscookie/CookieSerializationException.java @@ -0,0 +1,8 @@ +package org.jscookie; + +class CookieSerializationException extends Exception { + private static final long serialVersionUID = 1; + CookieSerializationException( Throwable cause ) { + super( cause ); + } +} diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 90b0798..0797925 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -6,6 +6,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.ResourceBundle; @@ -15,6 +16,7 @@ import org.eclipse.jdt.annotation.Nullable; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -69,22 +71,22 @@ public synchronized String get( String name ) { } @Override - public T get( String name, Class dataType ) throws ParseException { + public T get( String name, Class dataType ) throws CookieParseException { String value = get( name ); try { return mapper.readValue( value, dataType ); } catch ( IOException e ) { - throw new ParseException( e ); + throw new CookieParseException( e ); } } @Override - public T get( String name, TypeReference typeRef ) throws ParseException { + public T get( String name, TypeReference typeRef ) throws CookieParseException { String value = get( name ); try { return mapper.readValue( value, typeRef ); } catch ( IOException e ) { - throw new ParseException( e ); + throw new CookieParseException( e ); } } @@ -153,13 +155,30 @@ public synchronized void set( String name, String value, CookiesDefinition.Attri } @Override - public void set( String name, int value, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { - set( name, String.valueOf( value ), attributes ); + public void set( String name, int value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { + try { + set( name, String.valueOf( value ), attributes ); + } catch ( UnsupportedEncodingException e ) { + throw new CookieSerializationException( e ); + } + } + + @Override + public void set( String name, boolean value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { + try { + set( name, String.valueOf( value ), attributes ); + } catch ( UnsupportedEncodingException e ) { + throw new CookieSerializationException( e ); + } } @Override - public void set( String name, boolean value, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { - set( name, String.valueOf( value ), attributes ); + public void set( String name, List value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { + try { + set( name, mapper.writeValueAsString( value ), attributes ); + } catch ( UnsupportedEncodingException | JsonProcessingException e ) { + throw new CookieSerializationException( e ); + } } @Override @@ -174,13 +193,22 @@ public synchronized void set( String name, String value ) throws UnsupportedEnco } @Override - public void set( String name, int value ) throws UnsupportedEncodingException { + public void set( String name, int value ) throws CookieSerializationException { set( name, value, new Attributes() ); } @Override - public void set( String name, boolean value ) throws UnsupportedEncodingException { - set( name, String.valueOf( value ) ); + public void set( String name, boolean value ) throws CookieSerializationException { + try { + set( name, String.valueOf( value ) ); + } catch ( UnsupportedEncodingException e ) { + throw new CookieSerializationException( e ); + } + } + + @Override + public void set( String name, List value ) throws CookieSerializationException { + set( name, value, new Attributes() ); } @Override diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index 514a840..684b237 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -1,6 +1,7 @@ package org.jscookie; import java.io.UnsupportedEncodingException; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.core.type.TypeReference; @@ -18,10 +19,10 @@ public interface CookiesDefinition { /** * Retrieves a cookie and parse it using the given dataType instance * - * @throws ParseException + * @throws CookieParseException * If there's an error while parsing the cookie name using the given dataType */ - T get( String name, Class dataType ) throws ParseException; + T get( String name, Class dataType ) throws CookieParseException; /** * Retrieves a cookie and parse it using the given type reference. The type reference is used @@ -29,12 +30,12 @@ public interface CookiesDefinition { * * For more information check http://wiki.fasterxml.com/JacksonDataBinding#Full_Data_Binding * - * @throws ParseException + * @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 ParseException; + T get( String name, TypeReference typeRef ) throws CookieParseException; /** * Retrieves all cookies @@ -61,14 +62,21 @@ public interface CookiesDefinition { * * @see #set(String, String, Attributes) */ - void set( String name, int value, Attributes attributes ) throws UnsupportedEncodingException; + void set( String name, int value, Attributes attributes ) throws CookieSerializationException; /** * Create or update an existing cookie extending the default attributes and serializing the typed value * * @see #set(String, String, Attributes) */ - void set( String name, boolean value, Attributes attributes ) throws UnsupportedEncodingException; + void set( String name, boolean value, Attributes attributes ) throws CookieSerializationException; + + /** + * Create or update an existing cookie extending the default attributes and serializing the typed value + * + * @see #set(String, String, Attributes) + */ + void set( String name, List value, Attributes attributes ) throws CookieSerializationException; /** * Create or update an existing cookie using the default attributes @@ -82,14 +90,21 @@ public interface CookiesDefinition { * * @see #set(String, String) */ - void set( String name, int value ) throws UnsupportedEncodingException; + 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, boolean value ) throws UnsupportedEncodingException; + void set( String name, List value ) throws CookieSerializationException; /** * Remove an existing cookie
diff --git a/src/main/java/org/jscookie/ParseException.java b/src/main/java/org/jscookie/ParseException.java deleted file mode 100644 index 25ce649..0000000 --- a/src/main/java/org/jscookie/ParseException.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.jscookie; - -class ParseException extends Exception { - private static final long serialVersionUID = 1; - ParseException( Throwable cause ) { - super( cause ); - } - ParseException( String msg ) { - super( msg ); - } -} diff --git a/src/test/java/org/jscookie/CookiesJSONReadTest.java b/src/test/java/org/jscookie/CookiesJSONReadTest.java index 56226bf..262fc6d 100644 --- a/src/test/java/org/jscookie/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/CookiesJSONReadTest.java @@ -25,7 +25,7 @@ public void before() { } @Test - public void read_int_type() throws ParseException { + public void read_int_type() throws CookieParseException { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie( "c", "1" ) }); @@ -40,7 +40,7 @@ public void read_int_type() throws ParseException { } @Test - public void read_boolean_type() throws ParseException { + public void read_boolean_type() throws CookieParseException { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie( "c", "true" ) }); @@ -55,7 +55,7 @@ public void read_boolean_type() throws ParseException { } @Test - public void read_JSON_array_with_string() throws ParseException { + public void read_JSON_array_with_string() throws CookieParseException { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie( "c", "[\"v\"]" ) }); @@ -70,7 +70,7 @@ public void read_JSON_array_with_string() throws ParseException { } @Test - public void read_custom_type_with_string_prop() throws ParseException { + public void read_custom_type_with_string_prop() throws CookieParseException { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie( "c", "{\"property\":\"v\"}" ) }); @@ -85,7 +85,7 @@ public void read_custom_type_with_string_prop() throws ParseException { } @Test - public void read_custom_type_with_boolean_prop() throws ParseException { + public void read_custom_type_with_boolean_prop() throws CookieParseException { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie( "c", "{\"property\":true}" ) }); @@ -100,7 +100,7 @@ public void read_custom_type_with_boolean_prop() throws ParseException { } @Test - public void read_custom_type_with_number_prop() throws ParseException { + public void read_custom_type_with_number_prop() throws CookieParseException { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie( "c", "{\"property\":1}" ) }); diff --git a/src/test/java/org/jscookie/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/CookiesJSONWriteTest.java index 3d4743f..3259879 100644 --- a/src/test/java/org/jscookie/CookiesJSONWriteTest.java +++ b/src/test/java/org/jscookie/CookiesJSONWriteTest.java @@ -1,6 +1,6 @@ package org.jscookie; -import java.io.UnsupportedEncodingException; +import java.util.Arrays; import javax.servlet.http.Cookie; @@ -23,7 +23,7 @@ public void before() { } @Test - public void write_int_type() throws UnsupportedEncodingException { + public void write_int_type() throws CookieSerializationException { cookies.set( "c", 1 ); ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); @@ -34,7 +34,7 @@ public void write_int_type() throws UnsupportedEncodingException { } @Test - public void write_boolean_type() throws UnsupportedEncodingException { + public void write_boolean_type() throws CookieSerializationException { cookies.set( "c", true ); ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); @@ -44,17 +44,17 @@ public void write_boolean_type() throws UnsupportedEncodingException { Assert.assertEquals( "true", actual.getValue() ); } -// @Test -// public void write_JSON_array_with_string() { -// cookies.set( "c", Arrays.asList( "v" ) ); -// -// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); -// Mockito.verify( response ).addCookie( argument.capture() ); -// -// Cookie actual = argument.getValue(); -// Assert.assertEquals( "v", actual.getValue() ); -// } -// + @Test + public void write_JSON_array_with_string() throws CookieSerializationException { + cookies.set( "c", Arrays.asList( "v" ) ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "%5B%22v%22%5D", actual.getValue() ); + } + // @Test // public void write_custom_type_with_string_prop() { // cookies.set( "c", new CustomTypeString( "v" ) ); From b9d9645210afb8ffa19c1b5f7959e204978148a6 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Fri, 1 May 2015 16:34:54 -0300 Subject: [PATCH 16/63] This is just for internal purposes --- src/main/java/org/jscookie/CookiesDefinition.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index 684b237..e64cedd 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.core.type.TypeReference; -public interface CookiesDefinition { +interface CookiesDefinition { /** * Retrieves a cookie
* By default, assumes the characters not allowed in each cookie name are encoded with each From 10948a0e68c1b36fd2fe4e1b02c063161104bad3 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Fri, 1 May 2015 18:03:09 -0300 Subject: [PATCH 17/63] Serialize a custom type to JSON --- src/main/java/org/jscookie/CookieValue.java | 7 ++ src/main/java/org/jscookie/Cookies.java | 14 +++ .../java/org/jscookie/CookiesDefinition.java | 14 +++ .../org/jscookie/CookiesJSONWriteTest.java | 115 ++++++++++-------- 4 files changed, 97 insertions(+), 53 deletions(-) create mode 100644 src/main/java/org/jscookie/CookieValue.java 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 index 0797925..0c59191 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -181,6 +181,15 @@ public void set( String name, List value, CookiesDefinition.Attributes at } } + @Override + public void set( String name, CookieValue value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { + try { + set( name, mapper.writeValueAsString( value ), attributes ); + } catch ( UnsupportedEncodingException | JsonProcessingException e ) { + throw new CookieSerializationException( e ); + } + } + @Override public synchronized void set( String name, String value ) throws UnsupportedEncodingException { if ( name == null || name.length() == 0 ) { @@ -211,6 +220,11 @@ public void set( String name, List value ) throws CookieSerializationExce set( name, value, new Attributes() ); } + @Override + public void set( String name, CookieValue value ) throws CookieSerializationException { + set( name, value, new Attributes() ); + } + @Override public synchronized void remove( String name, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { if ( name == null || name.length() == 0 ) { diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index e64cedd..a6ebb82 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -78,6 +78,13 @@ interface CookiesDefinition { */ void set( String name, List value, Attributes attributes ) throws CookieSerializationException; + /** + * Create or update an existing cookie extending the default attributes and serializing the typed value + * + * @see #set(String, String, Attributes) + */ + void set( String name, CookieValue value, Attributes attributes ) throws CookieSerializationException; + /** * Create or update an existing cookie using the default attributes * @@ -106,6 +113,13 @@ interface CookiesDefinition { */ 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 diff --git a/src/test/java/org/jscookie/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/CookiesJSONWriteTest.java index 3259879..a1f336a 100644 --- a/src/test/java/org/jscookie/CookiesJSONWriteTest.java +++ b/src/test/java/org/jscookie/CookiesJSONWriteTest.java @@ -55,57 +55,66 @@ public void write_JSON_array_with_string() throws CookieSerializationException { Assert.assertEquals( "%5B%22v%22%5D", actual.getValue() ); } -// @Test -// public void write_custom_type_with_string_prop() { -// cookies.set( "c", new CustomTypeString( "v" ) ); -// -// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); -// Mockito.verify( response ).addCookie( argument.capture() ); -// -// Cookie actual = argument.getValue(); -// Assert.assertEquals( "v", actual.getValue() ); -// } -// -// @Test -// public void write_custom_type_with_boolean_prop() { -// cookies.set( "c", new CustomTypeBoolean( true ) ); -// -// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); -// Mockito.verify( response ).addCookie( argument.capture() ); -// -// Cookie actual = argument.getValue(); -// Assert.assertEquals( "true", actual.getValue() ); -// } -// -// @Test -// public void write_custom_type_with_number_prop() { -// cookies.set( "c", new CustomTypeInteger( 1 ) ); -// -// ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); -// Mockito.verify( response ).addCookie( argument.capture() ); -// -// Cookie actual = argument.getValue(); -// Assert.assertEquals( "1", actual.getValue() ); -// } -// -// private class CustomTypeString { -// private String property; -// private CustomTypeString( String property ) { -// this.property = property; -// } -// } -// -// private class CustomTypeBoolean { -// private Boolean property; -// private CustomTypeBoolean( Boolean property ) { -// this.property = property; -// } -// } -// -// private class CustomTypeInteger { -// private Integer property; -// private CustomTypeInteger( Integer property ) { -// this.property = property; -// } -// } + @Test + public void write_custom_type_with_string_prop() throws CookieSerializationException { + cookies.set( "c", new CustomTypeString( "v" ) ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "%7B%22property%22%3A%22v%22%7D", actual.getValue() ); + } + + @Test + public void write_custom_type_with_boolean_prop() throws CookieSerializationException { + cookies.set( "c", new CustomTypeBoolean( true ) ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "%7B%22property%22%3Atrue%7D", actual.getValue() ); + } + + @Test + public void write_custom_type_with_number_prop() throws CookieSerializationException { + cookies.set( "c", new CustomTypeInteger( 1 ) ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "1", actual.getValue() ); + } + + 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; + } + } } From c6cfd7a27b48f0a196f6aa3660f2cdaf67995635 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Fri, 1 May 2015 18:13:59 -0300 Subject: [PATCH 18/63] Fix test expectation --- src/test/java/org/jscookie/CookiesJSONWriteTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/jscookie/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/CookiesJSONWriteTest.java index a1f336a..c6285f8 100644 --- a/src/test/java/org/jscookie/CookiesJSONWriteTest.java +++ b/src/test/java/org/jscookie/CookiesJSONWriteTest.java @@ -85,7 +85,7 @@ public void write_custom_type_with_number_prop() throws CookieSerializationExcep Mockito.verify( response ).addCookie( argument.capture() ); Cookie actual = argument.getValue(); - Assert.assertEquals( "1", actual.getValue() ); + Assert.assertEquals( "%7B%22property%22%3A1%7D", actual.getValue() ); } class CustomTypeString implements CookieValue { From 0717129c5c2409e4b7e64e42c7bc4e82f409ca6c Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Fri, 1 May 2015 18:14:35 -0300 Subject: [PATCH 19/63] Use the null object pattern and prevent using the default constructor --- src/main/java/org/jscookie/Cookies.java | 20 ++++++++++++------- .../java/org/jscookie/CookiesWriteTest.java | 8 ++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 0c59191..e200298 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -23,7 +23,7 @@ public final class Cookies implements CookiesDefinition { private HttpServletRequest request; private HttpServletResponse response; - private CookiesDefinition.Attributes defaults = new Attributes(); + private CookiesDefinition.Attributes defaults = Attributes.empty(); private CookiesDefinition.Converter converter; private ObjectMapper mapper = new ObjectMapper(); @@ -203,7 +203,7 @@ public synchronized void set( String name, String value ) throws UnsupportedEnco @Override public void set( String name, int value ) throws CookieSerializationException { - set( name, value, new Attributes() ); + set( name, value, Attributes.empty() ); } @Override @@ -217,12 +217,12 @@ public void set( String name, boolean value ) throws CookieSerializationExceptio @Override public void set( String name, List value ) throws CookieSerializationException { - set( name, value, new Attributes() ); + set( name, value, Attributes.empty() ); } @Override public void set( String name, CookieValue value ) throws CookieSerializationException { - set( name, value, new Attributes() ); + set( name, value, Attributes.empty() ); } @Override @@ -234,7 +234,7 @@ public synchronized void remove( String name, CookiesDefinition.Attributes attri throw new IllegalArgumentException(); } - set( name, "", extend( attributes, new Attributes() + set( name, "", extend( attributes, Attributes.empty() .expires( Expiration.days( -1 ) )) ); } @@ -244,7 +244,7 @@ public synchronized void remove( String name ) throws UnsupportedEncodingExcepti if ( name == null || name.length() == 0 ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } - remove( name, new Attributes() ); + remove( name, Attributes.empty() ); } @Override @@ -261,7 +261,7 @@ public Cookies withConverter( CookiesDefinition.Converter converter ) { } private Attributes extend( CookiesDefinition.Attributes a, CookiesDefinition.Attributes b ) { - return new Attributes().merge( a ).merge( b ); + return Attributes.empty().merge( a ).merge( b ); } private String decodeName( Cookie cookie ) { @@ -301,6 +301,12 @@ public static class Attributes extends CookiesDefinition.Attributes { private String domain; private Boolean secure; + private Attributes() {} + + public static Attributes empty() { + return new Attributes(); + } + @Override @Nullable Expiration expires() { diff --git a/src/test/java/org/jscookie/CookiesWriteTest.java b/src/test/java/org/jscookie/CookiesWriteTest.java index ba5f3c5..e811629 100644 --- a/src/test/java/org/jscookie/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/CookiesWriteTest.java @@ -40,7 +40,7 @@ public void simple_write() throws UnsupportedEncodingException { @Test public void simple_write_with_default_attributes() throws UnsupportedEncodingException { - cookies.setDefaults(new Cookies.Attributes() + cookies.setDefaults(Cookies.Attributes.empty() .path( "/" ) .domain( "site.com" ) .secure( true ) @@ -62,7 +62,7 @@ public void simple_write_with_default_attributes() throws UnsupportedEncodingExc @Test public void simple_write_with_attributes() throws UnsupportedEncodingException { - cookies.set( "c", "v", new Cookies.Attributes() + cookies.set( "c", "v", Cookies.Attributes.empty() .path( "/" ) .domain( "example.com" ) .secure( true ) @@ -83,11 +83,11 @@ public void simple_write_with_attributes() throws UnsupportedEncodingException { @Test public void simple_write_overriding_default_attributes() throws UnsupportedEncodingException { - cookies.setDefaults(new Cookies.Attributes() + cookies.setDefaults(Cookies.Attributes.empty() .path( "/path/" ) .secure( true ) ); - cookies.set( "c", "v", new Cookies.Attributes() + cookies.set( "c", "v", Cookies.Attributes.empty() .path( "/" ) ); From ab6c7a8d4beb31fd151ba0c938f640dec87e7e82 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Fri, 1 May 2015 23:17:48 -0300 Subject: [PATCH 20/63] Rename the test packages to test only the public APIs By keeping the test classes in the same package I can reference a field with protected or default visibility that is not available publicly. In this case it is useful to test only the public APIs to ensure all relevant classes are available, nothing else. --- src/main/java/org/jscookie/CookieParseException.java | 2 +- .../java/org/jscookie/CookieSerializationException.java | 2 +- .../org/jscookie/{ => test/unit}/CookiesConverterTest.java | 6 ++++-- .../org/jscookie/{ => test/unit}/CookiesJSONReadTest.java | 6 ++++-- .../org/jscookie/{ => test/unit}/CookiesJSONWriteTest.java | 7 +++++-- .../java/org/jscookie/{ => test/unit}/CookiesReadTest.java | 5 +++-- .../org/jscookie/{ => test/unit}/CookiesWriteTest.java | 6 ++++-- .../jscookie/{testutils => test/unit/utils}/BaseTest.java | 2 +- 8 files changed, 23 insertions(+), 13 deletions(-) rename src/test/java/org/jscookie/{ => test/unit}/CookiesConverterTest.java (90%) rename src/test/java/org/jscookie/{ => test/unit}/CookiesJSONReadTest.java (96%) rename src/test/java/org/jscookie/{ => test/unit}/CookiesJSONWriteTest.java (94%) rename src/test/java/org/jscookie/{ => test/unit}/CookiesReadTest.java (91%) rename src/test/java/org/jscookie/{ => test/unit}/CookiesWriteTest.java (95%) rename src/test/java/org/jscookie/{testutils => test/unit/utils}/BaseTest.java (91%) diff --git a/src/main/java/org/jscookie/CookieParseException.java b/src/main/java/org/jscookie/CookieParseException.java index 685fdbb..8d951c9 100644 --- a/src/main/java/org/jscookie/CookieParseException.java +++ b/src/main/java/org/jscookie/CookieParseException.java @@ -1,6 +1,6 @@ package org.jscookie; -class CookieParseException extends Exception { +public class CookieParseException extends Exception { private static final long serialVersionUID = 1; CookieParseException( Throwable cause ) { super( cause ); diff --git a/src/main/java/org/jscookie/CookieSerializationException.java b/src/main/java/org/jscookie/CookieSerializationException.java index b8bc1fe..7a5d3cb 100644 --- a/src/main/java/org/jscookie/CookieSerializationException.java +++ b/src/main/java/org/jscookie/CookieSerializationException.java @@ -1,6 +1,6 @@ package org.jscookie; -class CookieSerializationException extends Exception { +public class CookieSerializationException extends Exception { private static final long serialVersionUID = 1; CookieSerializationException( Throwable cause ) { super( cause ); diff --git a/src/test/java/org/jscookie/CookiesConverterTest.java b/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java similarity index 90% rename from src/test/java/org/jscookie/CookiesConverterTest.java rename to src/test/java/org/jscookie/test/unit/CookiesConverterTest.java index 42df0df..bfcbaee 100644 --- a/src/test/java/org/jscookie/CookiesConverterTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java @@ -1,11 +1,13 @@ -package org.jscookie; +package org.jscookie.test.unit; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import javax.servlet.http.Cookie; -import org.jscookie.testutils.BaseTest; +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; diff --git a/src/test/java/org/jscookie/CookiesJSONReadTest.java b/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java similarity index 96% rename from src/test/java/org/jscookie/CookiesJSONReadTest.java rename to src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java index 262fc6d..0d7c024 100644 --- a/src/test/java/org/jscookie/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java @@ -1,10 +1,12 @@ -package org.jscookie; +package org.jscookie.test.unit; import java.util.List; import javax.servlet.http.Cookie; -import org.jscookie.testutils.BaseTest; +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; diff --git a/src/test/java/org/jscookie/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java similarity index 94% rename from src/test/java/org/jscookie/CookiesJSONWriteTest.java rename to src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java index c6285f8..71b1ba9 100644 --- a/src/test/java/org/jscookie/CookiesJSONWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java @@ -1,10 +1,13 @@ -package org.jscookie; +package org.jscookie.test.unit; import java.util.Arrays; import javax.servlet.http.Cookie; -import org.jscookie.testutils.BaseTest; +import org.jscookie.CookieSerializationException; +import org.jscookie.CookieValue; +import org.jscookie.Cookies; +import org.jscookie.test.unit.utils.BaseTest; import org.junit.Assert; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/org/jscookie/CookiesReadTest.java b/src/test/java/org/jscookie/test/unit/CookiesReadTest.java similarity index 91% rename from src/test/java/org/jscookie/CookiesReadTest.java rename to src/test/java/org/jscookie/test/unit/CookiesReadTest.java index 227801c..f2cd1bc 100644 --- a/src/test/java/org/jscookie/CookiesReadTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesReadTest.java @@ -1,10 +1,11 @@ -package org.jscookie; +package org.jscookie.test.unit; import java.util.Map; import javax.servlet.http.Cookie; -import org.jscookie.testutils.BaseTest; +import org.jscookie.Cookies; +import org.jscookie.test.unit.utils.BaseTest; import org.junit.Assert; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/org/jscookie/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java similarity index 95% rename from src/test/java/org/jscookie/CookiesWriteTest.java rename to src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index e811629..8b65398 100644 --- a/src/test/java/org/jscookie/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -1,10 +1,12 @@ -package org.jscookie; +package org.jscookie.test.unit; import java.io.UnsupportedEncodingException; import javax.servlet.http.Cookie; -import org.jscookie.testutils.BaseTest; +import org.jscookie.Cookies; +import org.jscookie.Expiration; +import org.jscookie.test.unit.utils.BaseTest; import org.junit.Assert; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/org/jscookie/testutils/BaseTest.java b/src/test/java/org/jscookie/test/unit/utils/BaseTest.java similarity index 91% rename from src/test/java/org/jscookie/testutils/BaseTest.java rename to src/test/java/org/jscookie/test/unit/utils/BaseTest.java index 6969b98..7d14750 100644 --- a/src/test/java/org/jscookie/testutils/BaseTest.java +++ b/src/test/java/org/jscookie/test/unit/utils/BaseTest.java @@ -1,4 +1,4 @@ -package org.jscookie.testutils; +package org.jscookie.test.unit.utils; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; From 539c8752c88b41181e9ee6fd87e0e96ac06534a4 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Fri, 1 May 2015 23:34:19 -0300 Subject: [PATCH 21/63] Let's synchronize only the main get/set methods --- src/main/java/org/jscookie/Cookies.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index e200298..29a39db 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -191,7 +191,7 @@ public void set( String name, CookieValue value, CookiesDefinition.Attributes at } @Override - public synchronized void set( String name, String value ) throws UnsupportedEncodingException { + public void set( String name, String value ) throws UnsupportedEncodingException { if ( name == null || name.length() == 0 ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } @@ -226,7 +226,7 @@ public void set( String name, CookieValue value ) throws CookieSerializationExce } @Override - public synchronized void remove( String name, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { + public void remove( String name, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { if ( name == null || name.length() == 0 ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } @@ -240,7 +240,7 @@ public synchronized void remove( String name, CookiesDefinition.Attributes attri } @Override - public synchronized void remove( String name ) throws UnsupportedEncodingException { + public void remove( String name ) throws UnsupportedEncodingException { if ( name == null || name.length() == 0 ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } From 24d5f86fcf29956f7a8273f54d8b1374d57620b9 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sat, 2 May 2015 21:25:35 -0300 Subject: [PATCH 22/63] Add arquillian with jboss EAP 6.4 managed container with servlet API --- .classpath | 6 ++ pom.xml | 85 ++++++++++++++++++- .../test/integration/CookiesEncodingIT.java | 43 ++++++++++ .../test/integration/TestServlet.java | 15 ++++ src/test/resources/arquillian.xml | 12 +++ 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/jscookie/test/integration/CookiesEncodingIT.java create mode 100644 src/test/java/org/jscookie/test/integration/TestServlet.java create mode 100644 src/test/resources/arquillian.xml diff --git a/.classpath b/.classpath index 7f30fa4..d8a3485 100644 --- a/.classpath +++ b/.classpath @@ -12,6 +12,12 @@
+ + + + + + diff --git a/pom.xml b/pom.xml index 6b9a40b..2089929 100644 --- a/pom.xml +++ b/pom.xml @@ -3,12 +3,12 @@ org.jscookie java-cookie 0.0.1-SNAPSHOT + Java Cookie + A simple Servlet API for handling cookies UTF-8 UTF-8 - Java Cookie - A complete Servlet API for handling cookies The MIT License (MIT) @@ -21,8 +21,51 @@ 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 @@ -46,6 +89,17 @@ + + + + org.jboss.arquillian + arquillian-bom + 1.1.8.Final + import + pom + + + javax.servlet @@ -74,6 +128,17 @@ 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.mockito mockito-all @@ -81,4 +146,20 @@ 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/test/java/org/jscookie/test/integration/CookiesEncodingIT.java b/src/test/java/org/jscookie/test/integration/CookiesEncodingIT.java new file mode 100644 index 0000000..a2c5533 --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/CookiesEncodingIT.java @@ -0,0 +1,43 @@ +package org.jscookie.test.integration; + +import java.net.URL; + +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.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith( Arquillian.class ) +public class CookiesEncodingIT { + @Deployment + public static Archive createDeployment() { + GenericArchive qunitFiles = ShrinkWrap.create( GenericArchive.class ) + .as( ExplodedImporter.class ) + .importDirectory( "src/main/webapp" ) + .as( GenericArchive.class ); + + WebArchive war = ShrinkWrap.create( WebArchive.class ) + .addPackage( "org.jscookie.test.integration" ) + .merge( qunitFiles, "/", Filters.includeAll() ); + + System.out.println( war.toString( true ) ); + + return war; + } + + @RunAsClient + @Test + public void read_qunit_test( @ArquillianResource URL baseURL ) { + System.out.println( "Available at: " + baseURL.toString() ); + Assert.assertTrue( true ); + } +} diff --git a/src/test/java/org/jscookie/test/integration/TestServlet.java b/src/test/java/org/jscookie/test/integration/TestServlet.java new file mode 100644 index 0000000..cd10b75 --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/TestServlet.java @@ -0,0 +1,15 @@ +package org.jscookie.test.integration; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet( urlPatterns = "/" ) +public class TestServlet extends HttpServlet { + private static final long serialVersionUID = 1; + @Override + public void doGet( HttpServletRequest request, HttpServletResponse response ) { + // TODO + } +} 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 + + + From 1e46ca80f65a61cf8b6cad123632ad619f290e47 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 3 May 2015 00:38:01 -0300 Subject: [PATCH 23/63] Integrate selenium with qunit --- pom.xml | 13 + src/main/webapp/encoding.html | 45 + src/main/webapp/qunit-1.18.0.css | 291 ++ src/main/webapp/qunit-1.18.0.js | 3828 +++++++++++++++++ .../test/integration/CookiesEncodingIT.java | 43 - .../test/integration/TestServlet.java | 15 - .../encoding/CookiesEncodingIT.java | 86 + .../encoding/EncodingPageObject.java | 21 + .../integration/encoding/EncodingServlet.java | 19 + .../integration/qunit/QUnitPageObject.java | 49 + .../test/integration/qunit/QUnitResults.java | 60 + 11 files changed, 4412 insertions(+), 58 deletions(-) create mode 100644 src/main/webapp/encoding.html create mode 100644 src/main/webapp/qunit-1.18.0.css create mode 100644 src/main/webapp/qunit-1.18.0.js delete mode 100644 src/test/java/org/jscookie/test/integration/CookiesEncodingIT.java delete mode 100644 src/test/java/org/jscookie/test/integration/TestServlet.java create mode 100644 src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java create mode 100644 src/test/java/org/jscookie/test/integration/encoding/EncodingPageObject.java create mode 100644 src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java create mode 100644 src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java create mode 100644 src/test/java/org/jscookie/test/integration/qunit/QUnitResults.java diff --git a/pom.xml b/pom.xml index 2089929..a47de24 100644 --- a/pom.xml +++ b/pom.xml @@ -8,6 +8,7 @@ UTF-8 UTF-8 + 2.45.0 @@ -122,6 +123,18 @@ jackson-databind 2.5.3 + + org.seleniumhq.selenium + selenium-java + ${selenium.version} + test + + + org.seleniumhq.selenium + selenium-firefox-driver + ${selenium.version} + test + junit junit diff --git a/src/main/webapp/encoding.html b/src/main/webapp/encoding.html new file mode 100644 index 0000000..66a9e4c --- /dev/null +++ b/src/main/webapp/encoding.html @@ -0,0 +1,45 @@ + + + + + QUnit Example + + + +
+
+ + + + \ No newline at end of file diff --git a/src/main/webapp/qunit-1.18.0.css b/src/main/webapp/qunit-1.18.0.css new file mode 100644 index 0000000..2c407b2 --- /dev/null +++ b/src/main/webapp/qunit-1.18.0.css @@ -0,0 +1,291 @@ +/*! + * QUnit 1.18.0 + * http://qunitjs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-04-03T10:23Z + */ + +/** Font Family and Sizes */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; +} + +#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } +#qunit-tests { font-size: smaller; } + + +/** Resets */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { + margin: 0; + padding: 0; +} + + +/** Header */ + +#qunit-header { + padding: 0.5em 0 0.5em 1em; + + color: #8699A4; + background-color: #0D3349; + + font-size: 1.5em; + line-height: 1em; + font-weight: 400; + + border-radius: 5px 5px 0 0; +} + +#qunit-header a { + text-decoration: none; + color: #C2CCD1; +} + +#qunit-header a:hover, +#qunit-header a:focus { + color: #FFF; +} + +#qunit-testrunner-toolbar label { + display: inline-block; + padding: 0 0.5em 0 0.1em; +} + +#qunit-banner { + height: 5px; +} + +#qunit-testrunner-toolbar { + padding: 0.5em 1em 0.5em 1em; + color: #5E740B; + background-color: #EEE; + overflow: hidden; +} + +#qunit-userAgent { + padding: 0.5em 1em 0.5em 1em; + background-color: #2B81AF; + color: #FFF; + text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +} + +#qunit-modulefilter-container { + float: right; + padding: 0.2em; +} + +.qunit-url-config { + display: inline-block; + padding: 0.1em; +} + +.qunit-filter { + display: block; + float: right; + margin-left: 1em; +} + +/** Tests: Pass/Fail */ + +#qunit-tests { + list-style-position: inside; +} + +#qunit-tests li { + padding: 0.4em 1em 0.4em 1em; + border-bottom: 1px solid #FFF; + list-style-position: inside; +} + +#qunit-tests > li { + display: none; +} + +#qunit-tests li.running, +#qunit-tests li.pass, +#qunit-tests li.fail, +#qunit-tests li.skipped { + display: list-item; +} + +#qunit-tests.hidepass li.running, +#qunit-tests.hidepass li.pass { + visibility: hidden; + position: absolute; + width: 0px; + height: 0px; + padding: 0; + border: 0; + margin: 0; +} + +#qunit-tests li strong { + cursor: pointer; +} + +#qunit-tests li.skipped strong { + cursor: default; +} + +#qunit-tests li a { + padding: 0.5em; + color: #C2CCD1; + text-decoration: none; +} + +#qunit-tests li p a { + padding: 0.25em; + color: #6B6464; +} +#qunit-tests li a:hover, +#qunit-tests li a:focus { + color: #000; +} + +#qunit-tests li .runtime { + float: right; + font-size: smaller; +} + +.qunit-assert-list { + margin-top: 0.5em; + padding: 0.5em; + + background-color: #FFF; + + border-radius: 5px; +} + +.qunit-collapsed { + display: none; +} + +#qunit-tests table { + border-collapse: collapse; + margin-top: 0.2em; +} + +#qunit-tests th { + text-align: right; + vertical-align: top; + padding: 0 0.5em 0 0; +} + +#qunit-tests td { + vertical-align: top; +} + +#qunit-tests pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +#qunit-tests del { + background-color: #E0F2BE; + color: #374E0C; + text-decoration: none; +} + +#qunit-tests ins { + background-color: #FFCACA; + color: #500; + text-decoration: none; +} + +/*** Test Counts */ + +#qunit-tests b.counts { color: #000; } +#qunit-tests b.passed { color: #5E740B; } +#qunit-tests b.failed { color: #710909; } + +#qunit-tests li li { + padding: 5px; + background-color: #FFF; + border-bottom: none; + list-style-position: inside; +} + +/*** Passing Styles */ + +#qunit-tests li li.pass { + color: #3C510C; + background-color: #FFF; + border-left: 10px solid #C6E746; +} + +#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } +#qunit-tests .pass .test-name { color: #366097; } + +#qunit-tests .pass .test-actual, +#qunit-tests .pass .test-expected { color: #999; } + +#qunit-banner.qunit-pass { background-color: #C6E746; } + +/*** Failing Styles */ + +#qunit-tests li li.fail { + color: #710909; + background-color: #FFF; + border-left: 10px solid #EE5757; + white-space: pre; +} + +#qunit-tests > li:last-child { + border-radius: 0 0 5px 5px; +} + +#qunit-tests .fail { color: #000; background-color: #EE5757; } +#qunit-tests .fail .test-name, +#qunit-tests .fail .module-name { color: #000; } + +#qunit-tests .fail .test-actual { color: #EE5757; } +#qunit-tests .fail .test-expected { color: #008000; } + +#qunit-banner.qunit-fail { background-color: #EE5757; } + +/*** Skipped tests */ + +#qunit-tests .skipped { + background-color: #EBECE9; +} + +#qunit-tests .qunit-skipped-label { + background-color: #F4FF77; + display: inline-block; + font-style: normal; + color: #366097; + line-height: 1.8em; + padding: 0 0.5em; + margin: -0.4em 0.4em -0.4em 0; +} + +/** Result */ + +#qunit-testresult { + padding: 0.5em 1em 0.5em 1em; + + color: #2B81AF; + background-color: #D2E0E6; + + border-bottom: 1px solid #FFF; +} +#qunit-testresult .module-name { + font-weight: 700; +} + +/** Fixture */ + +#qunit-fixture { + position: absolute; + top: -10000px; + left: -10000px; + width: 1000px; + height: 1000px; +} \ No newline at end of file diff --git a/src/main/webapp/qunit-1.18.0.js b/src/main/webapp/qunit-1.18.0.js new file mode 100644 index 0000000..4037de2 --- /dev/null +++ b/src/main/webapp/qunit-1.18.0.js @@ -0,0 +1,3828 @@ +/*! + * QUnit 1.18.0 + * http://qunitjs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-04-03T10:23Z + */ + +(function( window ) { + +var QUnit, + config, + onErrorFnPrev, + loggingCallbacks = {}, + fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ), + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + // Keep a local reference to Date (GH-283) + Date = window.Date, + now = Date.now || function() { + return new Date().getTime(); + }, + globalStartCalled = false, + runStarted = false, + setTimeout = window.setTimeout, + clearTimeout = window.clearTimeout, + defined = { + document: window.document !== undefined, + setTimeout: window.setTimeout !== undefined, + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch ( e ) { + return false; + } + }()) + }, + /** + * Provides a normalized error string, correcting an issue + * with IE 7 (and prior) where Error.prototype.toString is + * not properly implemented + * + * Based on http://es5.github.com/#x15.11.4.4 + * + * @param {String|Error} error + * @return {String} error message + */ + errorString = function( error ) { + var name, message, + errorString = error.toString(); + if ( errorString.substring( 0, 7 ) === "[object" ) { + name = error.name ? error.name.toString() : "Error"; + message = error.message ? error.message.toString() : ""; + if ( name && message ) { + return name + ": " + message; + } else if ( name ) { + return name; + } else if ( message ) { + return message; + } else { + return "Error"; + } + } else { + return errorString; + } + }, + /** + * Makes a clone of an object using only Array or Object as base, + * and copies over the own enumerable properties. + * + * @param {Object} obj + * @return {Object} New object with only the own properties (recursively). + */ + objectValues = function( obj ) { + var key, val, + vals = QUnit.is( "array", obj ) ? [] : {}; + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + val = obj[ key ]; + vals[ key ] = val === Object( val ) ? objectValues( val ) : val; + } + } + return vals; + }; + +QUnit = {}; + +/** + * Config object: Maintain internal state + * Later exposed as QUnit.config + * `config` initialized at top of scope + */ +config = { + // The queue of tests to run + queue: [], + + // block until document ready + blocking: true, + + // by default, run previously failed tests first + // very useful in combination with "Hide passed tests" checked + reorder: true, + + // by default, modify document.title when suite is done + altertitle: true, + + // by default, scroll to top of the page when suite is done + scrolltop: true, + + // when enabled, all tests must call expect() + requireExpects: false, + + // depth up-to which object will be dumped + maxDepth: 5, + + // add checkboxes that are persisted in the query-string + // when enabled, the id is set to `true` as a `QUnit.config` property + urlConfig: [ + { + id: "hidepassed", + label: "Hide passed tests", + tooltip: "Only show tests and assertions that fail. Stored as query-strings." + }, + { + id: "noglobals", + label: "Check for Globals", + tooltip: "Enabling this will test if any test introduces new properties on the " + + "`window` object. Stored as query-strings." + }, + { + id: "notrycatch", + label: "No try-catch", + tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + + "exceptions in IE reasonable. Stored as query-strings." + } + ], + + // Set of all modules. + modules: [], + + // The first unnamed module + currentModule: { + name: "", + tests: [] + }, + + callbacks: {} +}; + +// Push a loose unnamed module to the modules collection +config.modules.push( config.currentModule ); + +// Initialize more QUnit.config and QUnit.urlParams +(function() { + var i, current, + location = window.location || { search: "", protocol: "file:" }, + params = location.search.slice( 1 ).split( "&" ), + length = params.length, + urlParams = {}; + + if ( params[ 0 ] ) { + for ( i = 0; i < length; i++ ) { + current = params[ i ].split( "=" ); + current[ 0 ] = decodeURIComponent( current[ 0 ] ); + + // allow just a key to turn on a flag, e.g., test.html?noglobals + current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; + if ( urlParams[ current[ 0 ] ] ) { + urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); + } else { + urlParams[ current[ 0 ] ] = current[ 1 ]; + } + } + } + + if ( urlParams.filter === true ) { + delete urlParams.filter; + } + + QUnit.urlParams = urlParams; + + // String search anywhere in moduleName+testName + config.filter = urlParams.filter; + + if ( urlParams.maxDepth ) { + config.maxDepth = parseInt( urlParams.maxDepth, 10 ) === -1 ? + Number.POSITIVE_INFINITY : + urlParams.maxDepth; + } + + config.testId = []; + if ( urlParams.testId ) { + + // Ensure that urlParams.testId is an array + urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," ); + for ( i = 0; i < urlParams.testId.length; i++ ) { + config.testId.push( urlParams.testId[ i ] ); + } + } + + // Figure out if we're running the tests from a server or not + QUnit.isLocal = location.protocol === "file:"; + + // Expose the current QUnit version + QUnit.version = "1.18.0"; +}()); + +// Root QUnit object. +// `QUnit` initialized at top of scope +extend( QUnit, { + + // call on start of module test to prepend name to all tests + module: function( name, testEnvironment ) { + var currentModule = { + name: name, + testEnvironment: testEnvironment, + tests: [] + }; + + // DEPRECATED: handles setup/teardown functions, + // beforeEach and afterEach should be used instead + if ( testEnvironment && testEnvironment.setup ) { + testEnvironment.beforeEach = testEnvironment.setup; + delete testEnvironment.setup; + } + if ( testEnvironment && testEnvironment.teardown ) { + testEnvironment.afterEach = testEnvironment.teardown; + delete testEnvironment.teardown; + } + + config.modules.push( currentModule ); + config.currentModule = currentModule; + }, + + // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. + asyncTest: function( testName, expected, callback ) { + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + QUnit.test( testName, expected, callback, true ); + }, + + test: function( testName, expected, callback, async ) { + var test; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + test = new Test({ + testName: testName, + expected: expected, + async: async, + callback: callback + }); + + test.queue(); + }, + + skip: function( testName ) { + var test = new Test({ + testName: testName, + skip: true + }); + + test.queue(); + }, + + // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0. + // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior. + start: function( count ) { + var globalStartAlreadyCalled = globalStartCalled; + + if ( !config.current ) { + globalStartCalled = true; + + if ( runStarted ) { + throw new Error( "Called start() outside of a test context while already started" ); + } else if ( globalStartAlreadyCalled || count > 1 ) { + throw new Error( "Called start() outside of a test context too many times" ); + } else if ( config.autostart ) { + throw new Error( "Called start() outside of a test context when " + + "QUnit.config.autostart was true" ); + } else if ( !config.pageLoaded ) { + + // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it + config.autostart = true; + return; + } + } else { + + // If a test is running, adjust its semaphore + config.current.semaphore -= count || 1; + + // Don't start until equal number of stop-calls + if ( config.current.semaphore > 0 ) { + return; + } + + // throw an Error if start is called more often than stop + if ( config.current.semaphore < 0 ) { + config.current.semaphore = 0; + + QUnit.pushFailure( + "Called start() while already started (test's semaphore was 0 already)", + sourceFromStacktrace( 2 ) + ); + return; + } + } + + resumeProcessing(); + }, + + // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0. + stop: function( count ) { + + // If there isn't a test running, don't allow QUnit.stop() to be called + if ( !config.current ) { + throw new Error( "Called stop() outside of a test context" ); + } + + // If a test is running, adjust its semaphore + config.current.semaphore += count || 1; + + pauseProcessing(); + }, + + config: config, + + // Safe object type checking + is: function( type, obj ) { + return QUnit.objectType( obj ) === type; + }, + + objectType: function( obj ) { + if ( typeof obj === "undefined" ) { + return "undefined"; + } + + // Consider: typeof null === object + if ( obj === null ) { + return "null"; + } + + var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), + type = match && match[ 1 ] || ""; + + switch ( type ) { + case "Number": + if ( isNaN( obj ) ) { + return "nan"; + } + return "number"; + case "String": + case "Boolean": + case "Array": + case "Date": + case "RegExp": + case "Function": + return type.toLowerCase(); + } + if ( typeof obj === "object" ) { + return "object"; + } + return undefined; + }, + + extend: extend, + + load: function() { + config.pageLoaded = true; + + // Initialize the configuration options + extend( config, { + stats: { all: 0, bad: 0 }, + moduleStats: { all: 0, bad: 0 }, + started: 0, + updateRate: 1000, + autostart: true, + filter: "" + }, true ); + + config.blocking = false; + + if ( config.autostart ) { + resumeProcessing(); + } + } +}); + +// Register logging callbacks +(function() { + var i, l, key, + callbacks = [ "begin", "done", "log", "testStart", "testDone", + "moduleStart", "moduleDone" ]; + + function registerLoggingCallback( key ) { + var loggingCallback = function( callback ) { + if ( QUnit.objectType( callback ) !== "function" ) { + throw new Error( + "QUnit logging methods require a callback function as their first parameters." + ); + } + + config.callbacks[ key ].push( callback ); + }; + + // DEPRECATED: This will be removed on QUnit 2.0.0+ + // Stores the registered functions allowing restoring + // at verifyLoggingCallbacks() if modified + loggingCallbacks[ key ] = loggingCallback; + + return loggingCallback; + } + + for ( i = 0, l = callbacks.length; i < l; i++ ) { + key = callbacks[ i ]; + + // Initialize key collection of logging callback + if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) { + config.callbacks[ key ] = []; + } + + QUnit[ key ] = registerLoggingCallback( key ); + } +})(); + +// `onErrorFnPrev` initialized at top of scope +// Preserve other handlers +onErrorFnPrev = window.onerror; + +// Cover uncaught exceptions +// Returning true will suppress the default browser handler, +// returning false will let it run. +window.onerror = function( error, filePath, linerNr ) { + var ret = false; + if ( onErrorFnPrev ) { + ret = onErrorFnPrev( error, filePath, linerNr ); + } + + // Treat return value as window.onerror itself does, + // Only do our handling if not suppressed. + if ( ret !== true ) { + if ( QUnit.config.current ) { + if ( QUnit.config.current.ignoreGlobalErrors ) { + return true; + } + QUnit.pushFailure( error, filePath + ":" + linerNr ); + } else { + QUnit.test( "global failure", extend(function() { + QUnit.pushFailure( error, filePath + ":" + linerNr ); + }, { validTest: true } ) ); + } + return false; + } + + return ret; +}; + +function done() { + var runtime, passed; + + config.autorun = true; + + // Log the last module results + if ( config.previousModule ) { + runLoggingCallbacks( "moduleDone", { + name: config.previousModule.name, + tests: config.previousModule.tests, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all, + runtime: now() - config.moduleStats.started + }); + } + delete config.previousModule; + + runtime = now() - config.started; + passed = config.stats.all - config.stats.bad; + + runLoggingCallbacks( "done", { + failed: config.stats.bad, + passed: passed, + total: config.stats.all, + runtime: runtime + }); +} + +// Doesn't support IE6 to IE9, it will return undefined on these browsers +// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack +function extractStacktrace( e, offset ) { + offset = offset === undefined ? 4 : offset; + + var stack, include, i; + + if ( e.stack ) { + stack = e.stack.split( "\n" ); + if ( /^error$/i.test( stack[ 0 ] ) ) { + stack.shift(); + } + if ( fileName ) { + include = []; + for ( i = offset; i < stack.length; i++ ) { + if ( stack[ i ].indexOf( fileName ) !== -1 ) { + break; + } + include.push( stack[ i ] ); + } + if ( include.length ) { + return include.join( "\n" ); + } + } + return stack[ offset ]; + + // Support: Safari <=6 only + } else if ( e.sourceURL ) { + + // exclude useless self-reference for generated Error objects + if ( /qunit.js$/.test( e.sourceURL ) ) { + return; + } + + // for actual exceptions, this is useful + return e.sourceURL + ":" + e.line; + } +} + +function sourceFromStacktrace( offset ) { + var error = new Error(); + + // Support: Safari <=7 only, IE <=10 - 11 only + // Not all browsers generate the `stack` property for `new Error()`, see also #636 + if ( !error.stack ) { + try { + throw error; + } catch ( err ) { + error = err; + } + } + + return extractStacktrace( error, offset ); +} + +function synchronize( callback, last ) { + if ( QUnit.objectType( callback ) === "array" ) { + while ( callback.length ) { + synchronize( callback.shift() ); + } + return; + } + config.queue.push( callback ); + + if ( config.autorun && !config.blocking ) { + process( last ); + } +} + +function process( last ) { + function next() { + process( last ); + } + var start = now(); + config.depth = ( config.depth || 0 ) + 1; + + while ( config.queue.length && !config.blocking ) { + if ( !defined.setTimeout || config.updateRate <= 0 || + ( ( now() - start ) < config.updateRate ) ) { + if ( config.current ) { + + // Reset async tracking for each phase of the Test lifecycle + config.current.usedAsync = false; + } + config.queue.shift()(); + } else { + setTimeout( next, 13 ); + break; + } + } + config.depth--; + if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { + done(); + } +} + +function begin() { + var i, l, + modulesLog = []; + + // If the test run hasn't officially begun yet + if ( !config.started ) { + + // Record the time of the test run's beginning + config.started = now(); + + verifyLoggingCallbacks(); + + // Delete the loose unnamed module if unused. + if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) { + config.modules.shift(); + } + + // Avoid unnecessary information by not logging modules' test environments + for ( i = 0, l = config.modules.length; i < l; i++ ) { + modulesLog.push({ + name: config.modules[ i ].name, + tests: config.modules[ i ].tests + }); + } + + // The test run is officially beginning now + runLoggingCallbacks( "begin", { + totalTests: Test.count, + modules: modulesLog + }); + } + + config.blocking = false; + process( true ); +} + +function resumeProcessing() { + runStarted = true; + + // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.) + if ( defined.setTimeout ) { + setTimeout(function() { + if ( config.current && config.current.semaphore > 0 ) { + return; + } + if ( config.timeout ) { + clearTimeout( config.timeout ); + } + + begin(); + }, 13 ); + } else { + begin(); + } +} + +function pauseProcessing() { + config.blocking = true; + + if ( config.testTimeout && defined.setTimeout ) { + clearTimeout( config.timeout ); + config.timeout = setTimeout(function() { + if ( config.current ) { + config.current.semaphore = 0; + QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) ); + } else { + throw new Error( "Test timed out" ); + } + resumeProcessing(); + }, config.testTimeout ); + } +} + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in window ) { + if ( hasOwn.call( window, key ) ) { + // in Opera sometimes DOM element ids show up here, ignore them + if ( /^qunit-test-output/.test( key ) ) { + continue; + } + config.pollution.push( key ); + } + } + } +} + +function checkPollution() { + var newGlobals, + deletedGlobals, + old = config.pollution; + + saveGlobal(); + + newGlobals = diff( config.pollution, old ); + if ( newGlobals.length > 0 ) { + QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); + } + + deletedGlobals = diff( old, config.pollution ); + if ( deletedGlobals.length > 0 ) { + QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); + } +} + +// returns a new Array with the elements that are in a but not in b +function diff( a, b ) { + var i, j, + result = a.slice(); + + for ( i = 0; i < result.length; i++ ) { + for ( j = 0; j < b.length; j++ ) { + if ( result[ i ] === b[ j ] ) { + result.splice( i, 1 ); + i--; + break; + } + } + } + return result; +} + +function extend( a, b, undefOnly ) { + for ( var prop in b ) { + if ( hasOwn.call( b, prop ) ) { + + // Avoid "Member not found" error in IE8 caused by messing with window.constructor + if ( !( prop === "constructor" && a === window ) ) { + if ( b[ prop ] === undefined ) { + delete a[ prop ]; + } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { + a[ prop ] = b[ prop ]; + } + } + } + } + + return a; +} + +function runLoggingCallbacks( key, args ) { + var i, l, callbacks; + + callbacks = config.callbacks[ key ]; + for ( i = 0, l = callbacks.length; i < l; i++ ) { + callbacks[ i ]( args ); + } +} + +// DEPRECATED: This will be removed on 2.0.0+ +// This function verifies if the loggingCallbacks were modified by the user +// If so, it will restore it, assign the given callback and print a console warning +function verifyLoggingCallbacks() { + var loggingCallback, userCallback; + + for ( loggingCallback in loggingCallbacks ) { + if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) { + + userCallback = QUnit[ loggingCallback ]; + + // Restore the callback function + QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ]; + + // Assign the deprecated given callback + QUnit[ loggingCallback ]( userCallback ); + + if ( window.console && window.console.warn ) { + window.console.warn( + "QUnit." + loggingCallback + " was replaced with a new value.\n" + + "Please, check out the documentation on how to apply logging callbacks.\n" + + "Reference: http://api.qunitjs.com/category/callbacks/" + ); + } + } + } +} + +// from jquery.js +function inArray( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; +} + +function Test( settings ) { + var i, l; + + ++Test.count; + + extend( this, settings ); + this.assertions = []; + this.semaphore = 0; + this.usedAsync = false; + this.module = config.currentModule; + this.stack = sourceFromStacktrace( 3 ); + + // Register unique strings + for ( i = 0, l = this.module.tests; i < l.length; i++ ) { + if ( this.module.tests[ i ].name === this.testName ) { + this.testName += " "; + } + } + + this.testId = generateHash( this.module.name, this.testName ); + + this.module.tests.push({ + name: this.testName, + testId: this.testId + }); + + if ( settings.skip ) { + + // Skipped tests will fully ignore any sent callback + this.callback = function() {}; + this.async = false; + this.expected = 0; + } else { + this.assert = new Assert( this ); + } +} + +Test.count = 0; + +Test.prototype = { + before: function() { + if ( + + // Emit moduleStart when we're switching from one module to another + this.module !== config.previousModule || + + // They could be equal (both undefined) but if the previousModule property doesn't + // yet exist it means this is the first test in a suite that isn't wrapped in a + // module, in which case we'll just emit a moduleStart event for 'undefined'. + // Without this, reporters can get testStart before moduleStart which is a problem. + !hasOwn.call( config, "previousModule" ) + ) { + if ( hasOwn.call( config, "previousModule" ) ) { + runLoggingCallbacks( "moduleDone", { + name: config.previousModule.name, + tests: config.previousModule.tests, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all, + runtime: now() - config.moduleStats.started + }); + } + config.previousModule = this.module; + config.moduleStats = { all: 0, bad: 0, started: now() }; + runLoggingCallbacks( "moduleStart", { + name: this.module.name, + tests: this.module.tests + }); + } + + config.current = this; + + this.testEnvironment = extend( {}, this.module.testEnvironment ); + delete this.testEnvironment.beforeEach; + delete this.testEnvironment.afterEach; + + this.started = now(); + runLoggingCallbacks( "testStart", { + name: this.testName, + module: this.module.name, + testId: this.testId + }); + + if ( !config.pollution ) { + saveGlobal(); + } + }, + + run: function() { + var promise; + + config.current = this; + + if ( this.async ) { + QUnit.stop(); + } + + this.callbackStarted = now(); + + if ( config.notrycatch ) { + promise = this.callback.call( this.testEnvironment, this.assert ); + this.resolvePromise( promise ); + return; + } + + try { + promise = this.callback.call( this.testEnvironment, this.assert ); + this.resolvePromise( promise ); + } catch ( e ) { + this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); + + // else next test will carry the responsibility + saveGlobal(); + + // Restart the tests if they're blocking + if ( config.blocking ) { + QUnit.start(); + } + } + }, + + after: function() { + checkPollution(); + }, + + queueHook: function( hook, hookName ) { + var promise, + test = this; + return function runHook() { + config.current = test; + if ( config.notrycatch ) { + promise = hook.call( test.testEnvironment, test.assert ); + test.resolvePromise( promise, hookName ); + return; + } + try { + promise = hook.call( test.testEnvironment, test.assert ); + test.resolvePromise( promise, hookName ); + } catch ( error ) { + test.pushFailure( hookName + " failed on " + test.testName + ": " + + ( error.message || error ), extractStacktrace( error, 0 ) ); + } + }; + }, + + // Currently only used for module level hooks, can be used to add global level ones + hooks: function( handler ) { + var hooks = []; + + // Hooks are ignored on skipped tests + if ( this.skip ) { + return hooks; + } + + if ( this.module.testEnvironment && + QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) { + hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) ); + } + + return hooks; + }, + + finish: function() { + config.current = this; + if ( config.requireExpects && this.expected === null ) { + this.pushFailure( "Expected number of assertions to be defined, but expect() was " + + "not called.", this.stack ); + } else if ( this.expected !== null && this.expected !== this.assertions.length ) { + this.pushFailure( "Expected " + this.expected + " assertions, but " + + this.assertions.length + " were run", this.stack ); + } else if ( this.expected === null && !this.assertions.length ) { + this.pushFailure( "Expected at least one assertion, but none were run - call " + + "expect(0) to accept zero assertions.", this.stack ); + } + + var i, + bad = 0; + + this.runtime = now() - this.started; + config.stats.all += this.assertions.length; + config.moduleStats.all += this.assertions.length; + + for ( i = 0; i < this.assertions.length; i++ ) { + if ( !this.assertions[ i ].result ) { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + + runLoggingCallbacks( "testDone", { + name: this.testName, + module: this.module.name, + skipped: !!this.skip, + failed: bad, + passed: this.assertions.length - bad, + total: this.assertions.length, + runtime: this.runtime, + + // HTML Reporter use + assertions: this.assertions, + testId: this.testId, + + // DEPRECATED: this property will be removed in 2.0.0, use runtime instead + duration: this.runtime + }); + + // QUnit.reset() is deprecated and will be replaced for a new + // fixture reset function on QUnit 2.0/2.1. + // It's still called here for backwards compatibility handling + QUnit.reset(); + + config.current = undefined; + }, + + queue: function() { + var bad, + test = this; + + if ( !this.valid() ) { + return; + } + + function run() { + + // each of these can by async + synchronize([ + function() { + test.before(); + }, + + test.hooks( "beforeEach" ), + + function() { + test.run(); + }, + + test.hooks( "afterEach" ).reverse(), + + function() { + test.after(); + }, + function() { + test.finish(); + } + ]); + } + + // `bad` initialized at top of scope + // defer when previous test run passed, if storage is available + bad = QUnit.config.reorder && defined.sessionStorage && + +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); + + if ( bad ) { + run(); + } else { + synchronize( run, true ); + } + }, + + push: function( result, actual, expected, message ) { + var source, + details = { + module: this.module.name, + name: this.testName, + result: result, + message: message, + actual: actual, + expected: expected, + testId: this.testId, + runtime: now() - this.started + }; + + if ( !result ) { + source = sourceFromStacktrace(); + + if ( source ) { + details.source = source; + } + } + + runLoggingCallbacks( "log", details ); + + this.assertions.push({ + result: !!result, + message: message + }); + }, + + pushFailure: function( message, source, actual ) { + if ( !this instanceof Test ) { + throw new Error( "pushFailure() assertion outside test context, was " + + sourceFromStacktrace( 2 ) ); + } + + var details = { + module: this.module.name, + name: this.testName, + result: false, + message: message || "error", + actual: actual || null, + testId: this.testId, + runtime: now() - this.started + }; + + if ( source ) { + details.source = source; + } + + runLoggingCallbacks( "log", details ); + + this.assertions.push({ + result: false, + message: message + }); + }, + + resolvePromise: function( promise, phase ) { + var then, message, + test = this; + if ( promise != null ) { + then = promise.then; + if ( QUnit.objectType( then ) === "function" ) { + QUnit.stop(); + then.call( + promise, + QUnit.start, + function( error ) { + message = "Promise rejected " + + ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + + " " + test.testName + ": " + ( error.message || error ); + test.pushFailure( message, extractStacktrace( error, 0 ) ); + + // else next test will carry the responsibility + saveGlobal(); + + // Unblock + QUnit.start(); + } + ); + } + } + }, + + valid: function() { + var include, + filter = config.filter && config.filter.toLowerCase(), + module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(), + fullName = ( this.module.name + ": " + this.testName ).toLowerCase(); + + // Internally-generated tests are always valid + if ( this.callback && this.callback.validTest ) { + return true; + } + + if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) { + return false; + } + + if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) { + return false; + } + + if ( !filter ) { + return true; + } + + include = filter.charAt( 0 ) !== "!"; + if ( !include ) { + filter = filter.slice( 1 ); + } + + // If the filter matches, we need to honour include + if ( fullName.indexOf( filter ) !== -1 ) { + return include; + } + + // Otherwise, do the opposite + return !include; + } + +}; + +// Resets the test setup. Useful for tests that modify the DOM. +/* +DEPRECATED: Use multiple tests instead of resetting inside a test. +Use testStart or testDone for custom cleanup. +This method will throw an error in 2.0, and will be removed in 2.1 +*/ +QUnit.reset = function() { + + // Return on non-browser environments + // This is necessary to not break on node tests + if ( typeof window === "undefined" ) { + return; + } + + var fixture = defined.document && document.getElementById && + document.getElementById( "qunit-fixture" ); + + if ( fixture ) { + fixture.innerHTML = config.fixture; + } +}; + +QUnit.pushFailure = function() { + if ( !QUnit.config.current ) { + throw new Error( "pushFailure() assertion outside test context, in " + + sourceFromStacktrace( 2 ) ); + } + + // Gets current test obj + var currentTest = QUnit.config.current; + + return currentTest.pushFailure.apply( currentTest, arguments ); +}; + +// Based on Java's String.hashCode, a simple but not +// rigorously collision resistant hashing function +function generateHash( module, testName ) { + var hex, + i = 0, + hash = 0, + str = module + "\x1C" + testName, + len = str.length; + + for ( ; i < len; i++ ) { + hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); + hash |= 0; + } + + // Convert the possibly negative integer hash code into an 8 character hex string, which isn't + // strictly necessary but increases user understanding that the id is a SHA-like hash + hex = ( 0x100000000 + hash ).toString( 16 ); + if ( hex.length < 8 ) { + hex = "0000000" + hex; + } + + return hex.slice( -8 ); +} + +function Assert( testContext ) { + this.test = testContext; +} + +// Assert helpers +QUnit.assert = Assert.prototype = { + + // Specify the number of expected assertions to guarantee that failed test + // (no assertions are run at all) don't slip through. + expect: function( asserts ) { + if ( arguments.length === 1 ) { + this.test.expected = asserts; + } else { + return this.test.expected; + } + }, + + // Increment this Test's semaphore counter, then return a single-use function that + // decrements that counter a maximum of once. + async: function() { + var test = this.test, + popped = false; + + test.semaphore += 1; + test.usedAsync = true; + pauseProcessing(); + + return function done() { + if ( !popped ) { + test.semaphore -= 1; + popped = true; + resumeProcessing(); + } else { + test.pushFailure( "Called the callback returned from `assert.async` more than once", + sourceFromStacktrace( 2 ) ); + } + }; + }, + + // Exports test.push() to the user API + push: function( /* result, actual, expected, message */ ) { + var assert = this, + currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; + + // Backwards compatibility fix. + // Allows the direct use of global exported assertions and QUnit.assert.* + // Although, it's use is not recommended as it can leak assertions + // to other tests from async tests, because we only get a reference to the current test, + // not exactly the test where assertion were intended to be called. + if ( !currentTest ) { + throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); + } + + if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { + currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", + sourceFromStacktrace( 2 ) ); + + // Allow this assertion to continue running anyway... + } + + if ( !( assert instanceof Assert ) ) { + assert = currentTest.assert; + } + return assert.test.push.apply( assert.test, arguments ); + }, + + ok: function( result, message ) { + message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + + QUnit.dump.parse( result ) ); + this.push( !!result, result, true, message ); + }, + + notOk: function( result, message ) { + message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " + + QUnit.dump.parse( result ) ); + this.push( !result, result, false, message ); + }, + + equal: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + this.push( expected == actual, actual, expected, message ); + }, + + notEqual: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + this.push( expected != actual, actual, expected, message ); + }, + + propEqual: function( actual, expected, message ) { + actual = objectValues( actual ); + expected = objectValues( expected ); + this.push( QUnit.equiv( actual, expected ), actual, expected, message ); + }, + + notPropEqual: function( actual, expected, message ) { + actual = objectValues( actual ); + expected = objectValues( expected ); + this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); + }, + + deepEqual: function( actual, expected, message ) { + this.push( QUnit.equiv( actual, expected ), actual, expected, message ); + }, + + notDeepEqual: function( actual, expected, message ) { + this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); + }, + + strictEqual: function( actual, expected, message ) { + this.push( expected === actual, actual, expected, message ); + }, + + notStrictEqual: function( actual, expected, message ) { + this.push( expected !== actual, actual, expected, message ); + }, + + "throws": function( block, expected, message ) { + var actual, expectedType, + expectedOutput = expected, + ok = false, + currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current; + + // 'expected' is optional unless doing string comparison + if ( message == null && typeof expected === "string" ) { + message = expected; + expected = null; + } + + currentTest.ignoreGlobalErrors = true; + try { + block.call( currentTest.testEnvironment ); + } catch (e) { + actual = e; + } + currentTest.ignoreGlobalErrors = false; + + if ( actual ) { + expectedType = QUnit.objectType( expected ); + + // we don't want to validate thrown error + if ( !expected ) { + ok = true; + expectedOutput = null; + + // expected is a regexp + } else if ( expectedType === "regexp" ) { + ok = expected.test( errorString( actual ) ); + + // expected is a string + } else if ( expectedType === "string" ) { + ok = expected === errorString( actual ); + + // expected is a constructor, maybe an Error constructor + } else if ( expectedType === "function" && actual instanceof expected ) { + ok = true; + + // expected is an Error object + } else if ( expectedType === "object" ) { + ok = actual instanceof expected.constructor && + actual.name === expected.name && + actual.message === expected.message; + + // expected is a validation function which returns true if validation passed + } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { + expectedOutput = null; + ok = true; + } + } + + currentTest.assert.push( ok, actual, expectedOutput, message ); + } +}; + +// Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word +// Known to us are: Closure Compiler, Narwhal +(function() { + /*jshint sub:true */ + Assert.prototype.raises = Assert.prototype[ "throws" ]; +}()); + +// Test for equality any JavaScript type. +// Author: Philippe Rathé +QUnit.equiv = (function() { + + // Call the o related callback with the given arguments. + function bindCallbacks( o, callbacks, args ) { + var prop = QUnit.objectType( o ); + if ( prop ) { + if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { + return callbacks[ prop ].apply( callbacks, args ); + } else { + return callbacks[ prop ]; // or undefined + } + } + } + + // the real equiv function + var innerEquiv, + + // stack to decide between skip/abort functions + callers = [], + + // stack to avoiding loops from circular referencing + parents = [], + parentsB = [], + + getProto = Object.getPrototypeOf || function( obj ) { + /* jshint camelcase: false, proto: true */ + return obj.__proto__; + }, + callbacks = (function() { + + // for string, boolean, number and null + function useStrictEquality( b, a ) { + + /*jshint eqeqeq:false */ + if ( b instanceof a.constructor || a instanceof b.constructor ) { + + // to catch short annotation VS 'new' annotation of a + // declaration + // e.g. var i = 1; + // var j = new Number(1); + return a == b; + } else { + return a === b; + } + } + + return { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + + "nan": function( b ) { + return isNaN( b ); + }, + + "date": function( b, a ) { + return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function( b, a ) { + return QUnit.objectType( b ) === "regexp" && + + // the regex itself + a.source === b.source && + + // and its modifiers + a.global === b.global && + + // (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.sticky === b.sticky; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function() { + var caller = callers[ callers.length - 1 ]; + return caller !== Object && typeof caller !== "undefined"; + }, + + "array": function( b, a ) { + var i, j, len, loop, aCircular, bCircular; + + // b could be an object literal here + if ( QUnit.objectType( b ) !== "array" ) { + return false; + } + + len = a.length; + if ( len !== b.length ) { + // safe and faster + return false; + } + + // track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + for ( i = 0; i < len; i++ ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[ j ] === a[ i ]; + bCircular = parentsB[ j ] === b[ i ]; + if ( aCircular || bCircular ) { + if ( a[ i ] === b[ i ] || aCircular && bCircular ) { + loop = true; + } else { + parents.pop(); + parentsB.pop(); + return false; + } + } + } + if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { + parents.pop(); + parentsB.pop(); + return false; + } + } + parents.pop(); + parentsB.pop(); + return true; + }, + + "object": function( b, a ) { + + /*jshint forin:false */ + var i, j, loop, aCircular, bCircular, + // Default to true + eq = true, + aProperties = [], + bProperties = []; + + // comparing constructors is more strict than using + // instanceof + if ( a.constructor !== b.constructor ) { + + // Allow objects with no prototype to be equivalent to + // objects with Object as their constructor. + if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) || + ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) { + return false; + } + } + + // stack constructor before traversing properties + callers.push( a.constructor ); + + // track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + + // be strict: don't ensure hasOwnProperty and go deep + for ( i in a ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[ j ] === a[ i ]; + bCircular = parentsB[ j ] === b[ i ]; + if ( aCircular || bCircular ) { + if ( a[ i ] === b[ i ] || aCircular && bCircular ) { + loop = true; + } else { + eq = false; + break; + } + } + } + aProperties.push( i ); + if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { + eq = false; + break; + } + } + + parents.pop(); + parentsB.pop(); + callers.pop(); // unstack, we are done + + for ( i in b ) { + bProperties.push( i ); // collect b's properties + } + + // Ensures identical properties name + return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); + } + }; + }()); + + innerEquiv = function() { // can take multiple arguments + var args = [].slice.apply( arguments ); + if ( args.length < 2 ) { + return true; // end transition + } + + return ( (function( a, b ) { + if ( a === b ) { + return true; // catch the most you can + } else if ( a === null || b === null || typeof a === "undefined" || + typeof b === "undefined" || + QUnit.objectType( a ) !== QUnit.objectType( b ) ) { + + // don't lose time with error prone cases + return false; + } else { + return bindCallbacks( a, callbacks, [ b, a ] ); + } + + // apply transition with (1..n) arguments + }( args[ 0 ], args[ 1 ] ) ) && + innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); + }; + + return innerEquiv; +}()); + +// Based on jsDump by Ariel Flesler +// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html +QUnit.dump = (function() { + function quote( str ) { + return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; + } + function literal( o ) { + return o + ""; + } + function join( pre, arr, post ) { + var s = dump.separator(), + base = dump.indent(), + inner = dump.indent( 1 ); + if ( arr.join ) { + arr = arr.join( "," + s + inner ); + } + if ( !arr ) { + return pre + post; + } + return [ pre, inner + arr, base + post ].join( s ); + } + function array( arr, stack ) { + var i = arr.length, + ret = new Array( i ); + + if ( dump.maxDepth && dump.depth > dump.maxDepth ) { + return "[object Array]"; + } + + this.up(); + while ( i-- ) { + ret[ i ] = this.parse( arr[ i ], undefined, stack ); + } + this.down(); + return join( "[", ret, "]" ); + } + + var reName = /^function (\w+)/, + dump = { + + // objType is used mostly internally, you can fix a (custom) type in advance + parse: function( obj, objType, stack ) { + stack = stack || []; + var res, parser, parserType, + inStack = inArray( obj, stack ); + + if ( inStack !== -1 ) { + return "recursion(" + ( inStack - stack.length ) + ")"; + } + + objType = objType || this.typeOf( obj ); + parser = this.parsers[ objType ]; + parserType = typeof parser; + + if ( parserType === "function" ) { + stack.push( obj ); + res = parser.call( this, obj, stack ); + stack.pop(); + return res; + } + return ( parserType === "string" ) ? parser : this.parsers.error; + }, + typeOf: function( obj ) { + var type; + if ( obj === null ) { + type = "null"; + } else if ( typeof obj === "undefined" ) { + type = "undefined"; + } else if ( QUnit.is( "regexp", obj ) ) { + type = "regexp"; + } else if ( QUnit.is( "date", obj ) ) { + type = "date"; + } else if ( QUnit.is( "function", obj ) ) { + type = "function"; + } else if ( obj.setInterval !== undefined && + obj.document !== undefined && + obj.nodeType === undefined ) { + type = "window"; + } else if ( obj.nodeType === 9 ) { + type = "document"; + } else if ( obj.nodeType ) { + type = "node"; + } else if ( + + // native arrays + toString.call( obj ) === "[object Array]" || + + // NodeList objects + ( typeof obj.length === "number" && obj.item !== undefined && + ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && + obj[ 0 ] === undefined ) ) ) + ) { + type = "array"; + } else if ( obj.constructor === Error.prototype.constructor ) { + type = "error"; + } else { + type = typeof obj; + } + return type; + }, + separator: function() { + return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; + }, + // extra can be a number, shortcut for increasing-calling-decreasing + indent: function( extra ) { + if ( !this.multiline ) { + return ""; + } + var chr = this.indentChar; + if ( this.HTML ) { + chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); + } + return new Array( this.depth + ( extra || 0 ) ).join( chr ); + }, + up: function( a ) { + this.depth += a || 1; + }, + down: function( a ) { + this.depth -= a || 1; + }, + setParser: function( name, parser ) { + this.parsers[ name ] = parser; + }, + // The next 3 are exposed so you can use them + quote: quote, + literal: literal, + join: join, + // + depth: 1, + maxDepth: QUnit.config.maxDepth, + + // This is the list of parsers, to modify them, use dump.setParser + parsers: { + window: "[Window]", + document: "[Document]", + error: function( error ) { + return "Error(\"" + error.message + "\")"; + }, + unknown: "[Unknown]", + "null": "null", + "undefined": "undefined", + "function": function( fn ) { + var ret = "function", + + // functions never have name in IE + name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; + + if ( name ) { + ret += " " + name; + } + ret += "( "; + + ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); + return join( ret, dump.parse( fn, "functionCode" ), "}" ); + }, + array: array, + nodelist: array, + "arguments": array, + object: function( map, stack ) { + var keys, key, val, i, nonEnumerableProperties, + ret = []; + + if ( dump.maxDepth && dump.depth > dump.maxDepth ) { + return "[object Object]"; + } + + dump.up(); + keys = []; + for ( key in map ) { + keys.push( key ); + } + + // Some properties are not always enumerable on Error objects. + nonEnumerableProperties = [ "message", "name" ]; + for ( i in nonEnumerableProperties ) { + key = nonEnumerableProperties[ i ]; + if ( key in map && inArray( key, keys ) < 0 ) { + keys.push( key ); + } + } + keys.sort(); + for ( i = 0; i < keys.length; i++ ) { + key = keys[ i ]; + val = map[ key ]; + ret.push( dump.parse( key, "key" ) + ": " + + dump.parse( val, undefined, stack ) ); + } + dump.down(); + return join( "{", ret, "}" ); + }, + node: function( node ) { + var len, i, val, + open = dump.HTML ? "<" : "<", + close = dump.HTML ? ">" : ">", + tag = node.nodeName.toLowerCase(), + ret = open + tag, + attrs = node.attributes; + + if ( attrs ) { + for ( i = 0, len = attrs.length; i < len; i++ ) { + val = attrs[ i ].nodeValue; + + // IE6 includes all attributes in .attributes, even ones not explicitly + // set. Those have values like undefined, null, 0, false, "" or + // "inherit". + if ( val && val !== "inherit" ) { + ret += " " + attrs[ i ].nodeName + "=" + + dump.parse( val, "attribute" ); + } + } + } + ret += close; + + // Show content of TextNode or CDATASection + if ( node.nodeType === 3 || node.nodeType === 4 ) { + ret += node.nodeValue; + } + + return ret + open + "/" + tag + close; + }, + + // function calls it internally, it's the arguments part of the function + functionArgs: function( fn ) { + var args, + l = fn.length; + + if ( !l ) { + return ""; + } + + args = new Array( l ); + while ( l-- ) { + + // 97 is 'a' + args[ l ] = String.fromCharCode( 97 + l ); + } + return " " + args.join( ", " ) + " "; + }, + // object calls it internally, the key part of an item in a map + key: quote, + // function calls it internally, it's the content of the function + functionCode: "[code]", + // node calls it internally, it's an html attribute value + attribute: quote, + string: quote, + date: quote, + regexp: literal, + number: literal, + "boolean": literal + }, + // if true, entities are escaped ( <, >, \t, space and \n ) + HTML: false, + // indentation unit + indentChar: " ", + // if true, items in a collection, are separated by a \n, else just a space. + multiline: true + }; + + return dump; +}()); + +// back compat +QUnit.jsDump = QUnit.dump; + +// For browser, export only select globals +if ( typeof window !== "undefined" ) { + + // Deprecated + // Extend assert methods to QUnit and Global scope through Backwards compatibility + (function() { + var i, + assertions = Assert.prototype; + + function applyCurrent( current ) { + return function() { + var assert = new Assert( QUnit.config.current ); + current.apply( assert, arguments ); + }; + } + + for ( i in assertions ) { + QUnit[ i ] = applyCurrent( assertions[ i ] ); + } + })(); + + (function() { + var i, l, + keys = [ + "test", + "module", + "expect", + "asyncTest", + "start", + "stop", + "ok", + "notOk", + "equal", + "notEqual", + "propEqual", + "notPropEqual", + "deepEqual", + "notDeepEqual", + "strictEqual", + "notStrictEqual", + "throws" + ]; + + for ( i = 0, l = keys.length; i < l; i++ ) { + window[ keys[ i ] ] = QUnit[ keys[ i ] ]; + } + })(); + + window.QUnit = QUnit; +} + +// For nodejs +if ( typeof module !== "undefined" && module && module.exports ) { + module.exports = QUnit; + + // For consistency with CommonJS environments' exports + module.exports.QUnit = QUnit; +} + +// For CommonJS with exports, but without module.exports, like Rhino +if ( typeof exports !== "undefined" && exports ) { + exports.QUnit = QUnit; +} + +if ( typeof define === "function" && define.amd ) { + define( function() { + return QUnit; + } ); + QUnit.config.autostart = false; +} + +// Get a reference to the global object, like window in browsers +}( (function() { + return this; +})() )); + +/*istanbul ignore next */ +// jscs:disable maximumLineLength +/* + * This file is a modified version of google-diff-match-patch's JavaScript implementation + * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), + * modifications are licensed as more fully set forth in LICENSE.txt. + * + * The original source of google-diff-match-patch is attributable and licensed as follows: + * + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-diff-match-patch/ + * + * Licensed 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. + * + * More Info: + * https://code.google.com/p/google-diff-match-patch/ + * + * Usage: QUnit.diff(expected, actual) + * + * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) === "the quick brown fox jumpsed} Array of diff tuples. + */ + DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines, optDeadline ) { + var deadline, checklines, commonlength, + commonprefix, commonsuffix, diffs; + // Set a deadline by which time the diff must be complete. + if ( typeof optDeadline === "undefined" ) { + if ( this.DiffTimeout <= 0 ) { + optDeadline = Number.MAX_VALUE; + } else { + optDeadline = ( new Date() ).getTime() + this.DiffTimeout * 1000; + } + } + deadline = optDeadline; + + // Check for null inputs. + if ( text1 === null || text2 === null ) { + throw new Error( "Null input. (DiffMain)" ); + } + + // Check for equality (speedup). + if ( text1 === text2 ) { + if ( text1 ) { + return [ + [ DIFF_EQUAL, text1 ] + ]; + } + return []; + } + + if ( typeof optChecklines === "undefined" ) { + optChecklines = true; + } + + checklines = optChecklines; + + // Trim off common prefix (speedup). + commonlength = this.diffCommonPrefix( text1, text2 ); + commonprefix = text1.substring( 0, commonlength ); + text1 = text1.substring( commonlength ); + text2 = text2.substring( commonlength ); + + // Trim off common suffix (speedup). + ///////// + commonlength = this.diffCommonSuffix( text1, text2 ); + commonsuffix = text1.substring( text1.length - commonlength ); + text1 = text1.substring( 0, text1.length - commonlength ); + text2 = text2.substring( 0, text2.length - commonlength ); + + // Compute the diff on the middle block. + diffs = this.diffCompute( text1, text2, checklines, deadline ); + + // Restore the prefix and suffix. + if ( commonprefix ) { + diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); + } + if ( commonsuffix ) { + diffs.push( [ DIFF_EQUAL, commonsuffix ] ); + } + this.diffCleanupMerge( diffs ); + return diffs; + }; + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { + var changes, equalities, equalitiesLength, lastequality, + pointer, preIns, preDel, postIns, postDel; + changes = false; + equalities = []; // Stack of indices where equalities are found. + equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + lastequality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + preIns = false; + // Is there a deletion operation before the last equality. + preDel = false; + // Is there an insertion operation after the last equality. + postIns = false; + // Is there a deletion operation after the last equality. + postDel = false; + while ( pointer < diffs.length ) { + if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. + if ( diffs[ pointer ][ 1 ].length < this.DiffEditCost && ( postIns || postDel ) ) { + // Candidate found. + equalities[ equalitiesLength++ ] = pointer; + preIns = postIns; + preDel = postDel; + lastequality = diffs[ pointer ][ 1 ]; + } else { + // Not a candidate, and can never become one. + equalitiesLength = 0; + lastequality = null; + } + postIns = postDel = false; + } else { // An insertion or deletion. + if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { + postDel = true; + } else { + postIns = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || + ( ( lastequality.length < this.DiffEditCost / 2 ) && + ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { + // Duplicate record. + diffs.splice( equalities[equalitiesLength - 1], 0, [ DIFF_DELETE, lastequality ] ); + // Change second copy to insert. + diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; + equalitiesLength--; // Throw away the equality we just deleted; + lastequality = null; + if (preIns && preDel) { + // No changes made which could affect previous entry, keep going. + postIns = postDel = true; + equalitiesLength = 0; + } else { + equalitiesLength--; // Throw away the previous equality. + pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; + postIns = postDel = false; + } + changes = true; + } + } + pointer++; + } + + if ( changes ) { + this.diffCleanupMerge( diffs ); + } + }; + + /** + * Convert a diff array into a pretty HTML report. + * @param {!Array.} diffs Array of diff tuples. + * @param {integer} string to be beautified. + * @return {string} HTML representation. + */ + DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { + var op, data, x, html = []; + for ( x = 0; x < diffs.length; x++ ) { + op = diffs[x][0]; // Operation (insert, delete, equal) + data = diffs[x][1]; // Text of change. + switch ( op ) { + case DIFF_INSERT: + html[x] = "" + data + ""; + break; + case DIFF_DELETE: + html[x] = "" + data + ""; + break; + case DIFF_EQUAL: + html[x] = "" + data + ""; + break; + } + } + return html.join(""); + }; + + /** + * Determine the common prefix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ + DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { + var pointermid, pointermax, pointermin, pointerstart; + // Quick check for common null cases. + if ( !text1 || !text2 || text1.charAt(0) !== text2.charAt(0) ) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0; + pointermax = Math.min( text1.length, text2.length ); + pointermid = pointermax; + pointerstart = 0; + while ( pointermin < pointermid ) { + if ( text1.substring( pointerstart, pointermid ) === text2.substring( pointerstart, pointermid ) ) { + pointermin = pointermid; + pointerstart = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); + } + return pointermid; + }; + + /** + * Determine the common suffix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ + DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { + var pointermid, pointermax, pointermin, pointerend; + // Quick check for common null cases. + if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0; + pointermax = Math.min(text1.length, text2.length); + pointermid = pointermax; + pointerend = 0; + while ( pointermin < pointermid ) { + if (text1.substring( text1.length - pointermid, text1.length - pointerend ) === + text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { + pointermin = pointermid; + pointerend = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); + } + return pointermid; + }; + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean} checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster, slightly less optimal diff. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { + var diffs, longtext, shorttext, i, hm, + text1A, text2A, text1B, text2B, + midCommon, diffsA, diffsB; + + if ( !text1 ) { + // Just add some text (speedup). + return [ + [ DIFF_INSERT, text2 ] + ]; + } + + if (!text2) { + // Just delete some text (speedup). + return [ + [ DIFF_DELETE, text1 ] + ]; + } + + longtext = text1.length > text2.length ? text1 : text2; + shorttext = text1.length > text2.length ? text2 : text1; + i = longtext.indexOf( shorttext ); + if ( i !== -1 ) { + // Shorter text is inside the longer text (speedup). + diffs = [ + [ DIFF_INSERT, longtext.substring( 0, i ) ], + [ DIFF_EQUAL, shorttext ], + [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] + ]; + // Swap insertions for deletions if diff is reversed. + if ( text1.length > text2.length ) { + diffs[0][0] = diffs[2][0] = DIFF_DELETE; + } + return diffs; + } + + if ( shorttext.length === 1 ) { + // Single character string. + // After the previous speedup, the character can't be an equality. + return [ + [ DIFF_DELETE, text1 ], + [ DIFF_INSERT, text2 ] + ]; + } + + // Check to see if the problem can be split in two. + hm = this.diffHalfMatch(text1, text2); + if (hm) { + // A half-match was found, sort out the return data. + text1A = hm[0]; + text1B = hm[1]; + text2A = hm[2]; + text2B = hm[3]; + midCommon = hm[4]; + // Send both pairs off for separate processing. + diffsA = this.DiffMain(text1A, text2A, checklines, deadline); + diffsB = this.DiffMain(text1B, text2B, checklines, deadline); + // Merge the results. + return diffsA.concat([ + [ DIFF_EQUAL, midCommon ] + ], diffsB); + } + + if (checklines && text1.length > 100 && text2.length > 100) { + return this.diffLineMode(text1, text2, deadline); + } + + return this.diffBisect(text1, text2, deadline); + }; + + /** + * Do the two texts share a substring which is at least half the length of the + * longer text? + * This speedup can produce non-minimal diffs. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + * @private + */ + DiffMatchPatch.prototype.diffHalfMatch = function(text1, text2) { + var longtext, shorttext, dmp, + text1A, text2B, text2A, text1B, midCommon, + hm1, hm2, hm; + if (this.DiffTimeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + longtext = text1.length > text2.length ? text1 : text2; + shorttext = text1.length > text2.length ? text2 : text1; + if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { + return null; // Pointless. + } + dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Does a substring of shorttext exist within longtext such that the substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length substring within longtext. + * @return {Array.} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diffHalfMatchI(longtext, shorttext, i) { + var seed, j, bestCommon, prefixLength, suffixLength, + bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; + // Start with a 1/4 length substring at position i as a seed. + seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); + j = -1; + bestCommon = ""; + while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { + prefixLength = dmp.diffCommonPrefix(longtext.substring(i), + shorttext.substring(j)); + suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (bestCommon.length < suffixLength + prefixLength) { + bestCommon = shorttext.substring(j - suffixLength, j) + + shorttext.substring(j, j + prefixLength); + bestLongtextA = longtext.substring(0, i - suffixLength); + bestLongtextB = longtext.substring(i + prefixLength); + bestShorttextA = shorttext.substring(0, j - suffixLength); + bestShorttextB = shorttext.substring(j + prefixLength); + } + } + if (bestCommon.length * 2 >= longtext.length) { + return [ bestLongtextA, bestLongtextB, + bestShorttextA, bestShorttextB, bestCommon + ]; + } else { + return null; + } + } + + // First check if the second quarter is the seed for a half-match. + hm1 = diffHalfMatchI(longtext, shorttext, + Math.ceil(longtext.length / 4)); + // Check again based on the third quarter. + hm2 = diffHalfMatchI(longtext, shorttext, + Math.ceil(longtext.length / 2)); + if (!hm1 && !hm2) { + return null; + } else if (!hm2) { + hm = hm1; + } else if (!hm1) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length > hm2[4].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + text1A, text1B, text2A, text2B; + if (text1.length > text2.length) { + text1A = hm[0]; + text1B = hm[1]; + text2A = hm[2]; + text2B = hm[3]; + } else { + text2A = hm[0]; + text2B = hm[1]; + text1A = hm[2]; + text1B = hm[3]; + } + midCommon = hm[4]; + return [ text1A, text1B, text2A, text2B, midCommon ]; + }; + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffLineMode = function(text1, text2, deadline) { + var a, diffs, linearray, pointer, countInsert, + countDelete, textInsert, textDelete, j; + // Scan the text on a line-by-line basis first. + a = this.diffLinesToChars(text1, text2); + text1 = a.chars1; + text2 = a.chars2; + linearray = a.lineArray; + + diffs = this.DiffMain(text1, text2, false, deadline); + + // Convert the diff back to original text. + this.diffCharsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + this.diffCleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.push( [ DIFF_EQUAL, "" ] ); + pointer = 0; + countDelete = 0; + countInsert = 0; + textDelete = ""; + textInsert = ""; + while (pointer < diffs.length) { + switch ( diffs[pointer][0] ) { + case DIFF_INSERT: + countInsert++; + textInsert += diffs[pointer][1]; + break; + case DIFF_DELETE: + countDelete++; + textDelete += diffs[pointer][1]; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (countDelete >= 1 && countInsert >= 1) { + // Delete the offending records and add the merged ones. + diffs.splice(pointer - countDelete - countInsert, + countDelete + countInsert); + pointer = pointer - countDelete - countInsert; + a = this.DiffMain(textDelete, textInsert, false, deadline); + for (j = a.length - 1; j >= 0; j--) { + diffs.splice( pointer, 0, a[j] ); + } + pointer = pointer + a.length; + } + countInsert = 0; + countDelete = 0; + textDelete = ""; + textInsert = ""; + break; + } + pointer++; + } + diffs.pop(); // Remove the dummy entry at the end. + + return diffs; + }; + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffBisect = function(text1, text2, deadline) { + var text1Length, text2Length, maxD, vOffset, vLength, + v1, v2, x, delta, front, k1start, k1end, k2start, + k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; + // Cache the text lengths to prevent multiple calls. + text1Length = text1.length; + text2Length = text2.length; + maxD = Math.ceil((text1Length + text2Length) / 2); + vOffset = maxD; + vLength = 2 * maxD; + v1 = new Array(vLength); + v2 = new Array(vLength); + // Setting all elements to -1 is faster in Chrome & Firefox than mixing + // integers and undefined. + for (x = 0; x < vLength; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[vOffset + 1] = 0; + v2[vOffset + 1] = 0; + delta = text1Length - text2Length; + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + front = (delta % 2 !== 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + k1start = 0; + k1end = 0; + k2start = 0; + k2end = 0; + for (d = 0; d < maxD; d++) { + // Bail out if deadline is reached. + if ((new Date()).getTime() > deadline) { + break; + } + + // Walk the front path one step. + for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + k1Offset = vOffset + k1; + if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { + x1 = v1[k1Offset + 1]; + } else { + x1 = v1[k1Offset - 1] + 1; + } + y1 = x1 - k1; + while (x1 < text1Length && y1 < text2Length && + text1.charAt(x1) === text2.charAt(y1)) { + x1++; + y1++; + } + v1[k1Offset] = x1; + if (x1 > text1Length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2Length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + k2Offset = vOffset + delta - k1; + if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { + // Mirror x2 onto top-left coordinate system. + x2 = text1Length - v2[k2Offset]; + if (x1 >= x2) { + // Overlap detected. + return this.diffBisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + k2Offset = vOffset + k2; + if ( k2 === -d || (k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { + x2 = v2[k2Offset + 1]; + } else { + x2 = v2[k2Offset - 1] + 1; + } + y2 = x2 - k2; + while (x2 < text1Length && y2 < text2Length && + text1.charAt(text1Length - x2 - 1) === + text2.charAt(text2Length - y2 - 1)) { + x2++; + y2++; + } + v2[k2Offset] = x2; + if (x2 > text1Length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2Length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + k1Offset = vOffset + delta - k2; + if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { + x1 = v1[k1Offset]; + y1 = vOffset + x1 - k1Offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1Length - x2; + if (x1 >= x2) { + // Overlap detected. + return this.diffBisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [ + [ DIFF_DELETE, text1 ], + [ DIFF_INSERT, text2 ] + ]; + }; + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { + var text1a, text1b, text2a, text2b, diffs, diffsb; + text1a = text1.substring(0, x); + text2a = text2.substring(0, y); + text1b = text1.substring(x); + text2b = text2.substring(y); + + // Compute both diffs serially. + diffs = this.DiffMain(text1a, text2a, false, deadline); + diffsb = this.DiffMain(text1b, text2b, false, deadline); + + return diffs.concat(diffsb); + }; + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupSemantic = function(diffs) { + var changes, equalities, equalitiesLength, lastequality, + pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, + lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; + changes = false; + equalities = []; // Stack of indices where equalities are found. + equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + lastequality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + lengthInsertions1 = 0; + lengthDeletions1 = 0; + // Number of characters that changed after the equality. + lengthInsertions2 = 0; + lengthDeletions2 = 0; + while (pointer < diffs.length) { + if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found. + equalities[equalitiesLength++] = pointer; + lengthInsertions1 = lengthInsertions2; + lengthDeletions1 = lengthDeletions2; + lengthInsertions2 = 0; + lengthDeletions2 = 0; + lastequality = diffs[pointer][1]; + } else { // An insertion or deletion. + if (diffs[pointer][0] === DIFF_INSERT) { + lengthInsertions2 += diffs[pointer][1].length; + } else { + lengthDeletions2 += diffs[pointer][1].length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastequality && (lastequality.length <= + Math.max(lengthInsertions1, lengthDeletions1)) && + (lastequality.length <= Math.max(lengthInsertions2, + lengthDeletions2))) { + // Duplicate record. + diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] ); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + // Throw away the equality we just deleted. + equalitiesLength--; + // Throw away the previous equality (it needs to be reevaluated). + equalitiesLength--; + pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; + lengthInsertions1 = 0; // Reset the counters. + lengthDeletions1 = 0; + lengthInsertions2 = 0; + lengthDeletions2 = 0; + lastequality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if (changes) { + this.diffCleanupMerge(diffs); + } + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.length) { + if (diffs[pointer - 1][0] === DIFF_DELETE && + diffs[pointer][0] === DIFF_INSERT) { + deletion = diffs[pointer - 1][1]; + insertion = diffs[pointer][1]; + overlapLength1 = this.diffCommonOverlap(deletion, insertion); + overlapLength2 = this.diffCommonOverlap(insertion, deletion); + if (overlapLength1 >= overlapLength2) { + if (overlapLength1 >= deletion.length / 2 || + overlapLength1 >= insertion.length / 2) { + // Overlap found. Insert an equality and trim the surrounding edits. + diffs.splice( pointer, 0, [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] ); + diffs[pointer - 1][1] = + deletion.substring(0, deletion.length - overlapLength1); + diffs[pointer + 1][1] = insertion.substring(overlapLength1); + pointer++; + } + } else { + if (overlapLength2 >= deletion.length / 2 || + overlapLength2 >= insertion.length / 2) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.splice( pointer, 0, [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] ); + diffs[pointer - 1][0] = DIFF_INSERT; + diffs[pointer - 1][1] = + insertion.substring(0, insertion.length - overlapLength2); + diffs[pointer + 1][0] = DIFF_DELETE; + diffs[pointer + 1][1] = + deletion.substring(overlapLength2); + pointer++; + } + } + pointer++; + } + pointer++; + } + }; + + /** + * Determine if the suffix of one string is the prefix of another. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of the first + * string and the start of the second string. + * @private + */ + DiffMatchPatch.prototype.diffCommonOverlap = function(text1, text2) { + var text1Length, text2Length, textLength, + best, length, pattern, found; + // Cache the text lengths to prevent multiple calls. + text1Length = text1.length; + text2Length = text2.length; + // Eliminate the null case. + if (text1Length === 0 || text2Length === 0) { + return 0; + } + // Truncate the longer string. + if (text1Length > text2Length) { + text1 = text1.substring(text1Length - text2Length); + } else if (text1Length < text2Length) { + text2 = text2.substring(0, text1Length); + } + textLength = Math.min(text1Length, text2Length); + // Quick check for the worst case. + if (text1 === text2) { + return textLength; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + best = 0; + length = 1; + while (true) { + pattern = text1.substring(textLength - length); + found = text2.indexOf(pattern); + if (found === -1) { + return best; + } + length += found; + if (found === 0 || text1.substring(textLength - length) === + text2.substring(0, length)) { + best = length; + length++; + } + } + }; + + /** + * Split two texts into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {{chars1: string, chars2: string, lineArray: !Array.}} + * An object containing the encoded text1, the encoded text2 and + * the array of unique strings. + * The zeroth element of the array of unique strings is intentionally blank. + * @private + */ + DiffMatchPatch.prototype.diffLinesToChars = function(text1, text2) { + var lineArray, lineHash, chars1, chars2; + lineArray = []; // e.g. lineArray[4] === 'Hello\n' + lineHash = {}; // e.g. lineHash['Hello\n'] === 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray[0] = ""; + + /** + * Split a text into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * Modifies linearray and linehash through being a closure. + * @param {string} text String to encode. + * @return {string} Encoded string. + * @private + */ + function diffLinesToCharsMunge(text) { + var chars, lineStart, lineEnd, lineArrayLength, line; + chars = ""; + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + lineStart = 0; + lineEnd = -1; + // Keeping our own length variable is faster than looking it up. + lineArrayLength = lineArray.length; + while (lineEnd < text.length - 1) { + lineEnd = text.indexOf("\n", lineStart); + if (lineEnd === -1) { + lineEnd = text.length - 1; + } + line = text.substring(lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + + if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : + (lineHash[line] !== undefined)) { + chars += String.fromCharCode( lineHash[ line ] ); + } else { + chars += String.fromCharCode(lineArrayLength); + lineHash[line] = lineArrayLength; + lineArray[lineArrayLength++] = line; + } + } + return chars; + } + + chars1 = diffLinesToCharsMunge(text1); + chars2 = diffLinesToCharsMunge(text2); + return { + chars1: chars1, + chars2: chars2, + lineArray: lineArray + }; + }; + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param {!Array.} diffs Array of diff tuples. + * @param {!Array.} lineArray Array of unique strings. + * @private + */ + DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { + var x, chars, text, y; + for ( x = 0; x < diffs.length; x++ ) { + chars = diffs[x][1]; + text = []; + for ( y = 0; y < chars.length; y++ ) { + text[y] = lineArray[chars.charCodeAt(y)]; + } + diffs[x][1] = text.join(""); + } + }; + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupMerge = function(diffs) { + var pointer, countDelete, countInsert, textInsert, textDelete, + commonlength, changes; + diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. + pointer = 0; + countDelete = 0; + countInsert = 0; + textDelete = ""; + textInsert = ""; + commonlength; + while (pointer < diffs.length) { + switch ( diffs[ pointer ][ 0 ] ) { + case DIFF_INSERT: + countInsert++; + textInsert += diffs[pointer][1]; + pointer++; + break; + case DIFF_DELETE: + countDelete++; + textDelete += diffs[pointer][1]; + pointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (countDelete + countInsert > 1) { + if (countDelete !== 0 && countInsert !== 0) { + // Factor out any common prefixies. + commonlength = this.diffCommonPrefix(textInsert, textDelete); + if (commonlength !== 0) { + if ((pointer - countDelete - countInsert) > 0 && + diffs[pointer - countDelete - countInsert - 1][0] === + DIFF_EQUAL) { + diffs[pointer - countDelete - countInsert - 1][1] += + textInsert.substring(0, commonlength); + } else { + diffs.splice( 0, 0, [ DIFF_EQUAL, + textInsert.substring( 0, commonlength ) + ] ); + pointer++; + } + textInsert = textInsert.substring(commonlength); + textDelete = textDelete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = this.diffCommonSuffix(textInsert, textDelete); + if (commonlength !== 0) { + diffs[pointer][1] = textInsert.substring(textInsert.length - + commonlength) + diffs[pointer][1]; + textInsert = textInsert.substring(0, textInsert.length - + commonlength); + textDelete = textDelete.substring(0, textDelete.length - + commonlength); + } + } + // Delete the offending records and add the merged ones. + if (countDelete === 0) { + diffs.splice( pointer - countInsert, + countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); + } else if (countInsert === 0) { + diffs.splice( pointer - countDelete, + countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); + } else { + diffs.splice( pointer - countDelete - countInsert, + countDelete + countInsert, [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] ); + } + pointer = pointer - countDelete - countInsert + + (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; + } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1][1] += diffs[pointer][1]; + diffs.splice(pointer, 1); + } else { + pointer++; + } + countInsert = 0; + countDelete = 0; + textDelete = ""; + textInsert = ""; + break; + } + } + if (diffs[diffs.length - 1][1] === "") { + diffs.pop(); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] === DIFF_EQUAL && + diffs[pointer + 1][0] === DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + if ( diffs[ pointer ][ 1 ].substring( diffs[ pointer ][ 1 ].length - + diffs[ pointer - 1 ][ 1 ].length ) === diffs[ pointer - 1 ][ 1 ] ) { + // Shift the edit over the previous equality. + diffs[pointer][1] = diffs[pointer - 1][1] + + diffs[pointer][1].substring(0, diffs[pointer][1].length - + diffs[pointer - 1][1].length); + diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; + diffs.splice(pointer - 1, 1); + changes = true; + } else if ( diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === + diffs[ pointer + 1 ][ 1 ] ) { + // Shift the edit over the next equality. + diffs[pointer - 1][1] += diffs[pointer + 1][1]; + diffs[pointer][1] = + diffs[pointer][1].substring(diffs[pointer + 1][1].length) + + diffs[pointer + 1][1]; + diffs.splice(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + this.diffCleanupMerge(diffs); + } + }; + + return function(o, n) { + var diff, output, text; + diff = new DiffMatchPatch(); + output = diff.DiffMain(o, n); + //console.log(output); + diff.diffCleanupEfficiency(output); + text = diff.diffPrettyHtml(output); + + return text; + }; +}()); +// jscs:enable + +(function() { + +// Deprecated QUnit.init - Ref #530 +// Re-initialize the configuration options +QUnit.init = function() { + var tests, banner, result, qunit, + config = QUnit.config; + + config.stats = { all: 0, bad: 0 }; + config.moduleStats = { all: 0, bad: 0 }; + config.started = 0; + config.updateRate = 1000; + config.blocking = false; + config.autostart = true; + config.autorun = false; + config.filter = ""; + config.queue = []; + + // Return on non-browser environments + // This is necessary to not break on node tests + if ( typeof window === "undefined" ) { + return; + } + + qunit = id( "qunit" ); + if ( qunit ) { + qunit.innerHTML = + "

" + escapeText( document.title ) + "

" + + "

" + + "
" + + "

" + + "
    "; + } + + tests = id( "qunit-tests" ); + banner = id( "qunit-banner" ); + result = id( "qunit-testresult" ); + + if ( tests ) { + tests.innerHTML = ""; + } + + if ( banner ) { + banner.className = ""; + } + + if ( result ) { + result.parentNode.removeChild( result ); + } + + if ( tests ) { + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = "Running...
     "; + } +}; + +// Don't load the HTML Reporter on non-Browser environments +if ( typeof window === "undefined" ) { + return; +} + +var config = QUnit.config, + hasOwn = Object.prototype.hasOwnProperty, + defined = { + document: window.document !== undefined, + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch ( e ) { + return false; + } + }()) + }, + modulesList = []; + +/** +* Escape text for attribute or text content. +*/ +function escapeText( s ) { + if ( !s ) { + return ""; + } + s = s + ""; + + // Both single quotes and double quotes (for attributes) + return s.replace( /['"<>&]/g, function( s ) { + switch ( s ) { + case "'": + return "'"; + case "\"": + return """; + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + } + }); +} + +/** + * @param {HTMLElement} elem + * @param {string} type + * @param {Function} fn + */ +function addEvent( elem, type, fn ) { + if ( elem.addEventListener ) { + + // Standards-based browsers + elem.addEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + + // support: IE <9 + elem.attachEvent( "on" + type, function() { + var event = window.event; + if ( !event.target ) { + event.target = event.srcElement || document; + } + + fn.call( elem, event ); + }); + } +} + +/** + * @param {Array|NodeList} elems + * @param {string} type + * @param {Function} fn + */ +function addEvents( elems, type, fn ) { + var i = elems.length; + while ( i-- ) { + addEvent( elems[ i ], type, fn ); + } +} + +function hasClass( elem, name ) { + return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; +} + +function addClass( elem, name ) { + if ( !hasClass( elem, name ) ) { + elem.className += ( elem.className ? " " : "" ) + name; + } +} + +function toggleClass( elem, name ) { + if ( hasClass( elem, name ) ) { + removeClass( elem, name ); + } else { + addClass( elem, name ); + } +} + +function removeClass( elem, name ) { + var set = " " + elem.className + " "; + + // Class name may appear multiple times + while ( set.indexOf( " " + name + " " ) >= 0 ) { + set = set.replace( " " + name + " ", " " ); + } + + // trim for prettiness + elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); +} + +function id( name ) { + return defined.document && document.getElementById && document.getElementById( name ); +} + +function getUrlConfigHtml() { + var i, j, val, + escaped, escapedTooltip, + selection = false, + len = config.urlConfig.length, + urlConfigHtml = ""; + + for ( i = 0; i < len; i++ ) { + val = config.urlConfig[ i ]; + if ( typeof val === "string" ) { + val = { + id: val, + label: val + }; + } + + escaped = escapeText( val.id ); + escapedTooltip = escapeText( val.tooltip ); + + if ( config[ val.id ] === undefined ) { + config[ val.id ] = QUnit.urlParams[ val.id ]; + } + + if ( !val.value || typeof val.value === "string" ) { + urlConfigHtml += ""; + } else { + urlConfigHtml += ""; + } + } + + return urlConfigHtml; +} + +// Handle "click" events on toolbar checkboxes and "change" for select menus. +// Updates the URL with the new state of `config.urlConfig` values. +function toolbarChanged() { + var updatedUrl, value, + field = this, + params = {}; + + // Detect if field is a select menu or a checkbox + if ( "selectedIndex" in field ) { + value = field.options[ field.selectedIndex ].value || undefined; + } else { + value = field.checked ? ( field.defaultValue || true ) : undefined; + } + + params[ field.name ] = value; + updatedUrl = setUrl( params ); + + if ( "hidepassed" === field.name && "replaceState" in window.history ) { + config[ field.name ] = value || false; + if ( value ) { + addClass( id( "qunit-tests" ), "hidepass" ); + } else { + removeClass( id( "qunit-tests" ), "hidepass" ); + } + + // It is not necessary to refresh the whole page + window.history.replaceState( null, "", updatedUrl ); + } else { + window.location = updatedUrl; + } +} + +function setUrl( params ) { + var key, + querystring = "?"; + + params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); + + for ( key in params ) { + if ( hasOwn.call( params, key ) ) { + if ( params[ key ] === undefined ) { + continue; + } + querystring += encodeURIComponent( key ); + if ( params[ key ] !== true ) { + querystring += "=" + encodeURIComponent( params[ key ] ); + } + querystring += "&"; + } + } + return location.protocol + "//" + location.host + + location.pathname + querystring.slice( 0, -1 ); +} + +function applyUrlParams() { + var selectedModule, + modulesList = id( "qunit-modulefilter" ), + filter = id( "qunit-filter-input" ).value; + + selectedModule = modulesList ? + decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) : + undefined; + + window.location = setUrl({ + module: ( selectedModule === "" ) ? undefined : selectedModule, + filter: ( filter === "" ) ? undefined : filter, + + // Remove testId filter + testId: undefined + }); +} + +function toolbarUrlConfigContainer() { + var urlConfigContainer = document.createElement( "span" ); + + urlConfigContainer.innerHTML = getUrlConfigHtml(); + addClass( urlConfigContainer, "qunit-url-config" ); + + // For oldIE support: + // * Add handlers to the individual elements instead of the container + // * Use "click" instead of "change" for checkboxes + addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged ); + addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); + + return urlConfigContainer; +} + +function toolbarLooseFilter() { + var filter = document.createElement( "form" ), + label = document.createElement( "label" ), + input = document.createElement( "input" ), + button = document.createElement( "button" ); + + addClass( filter, "qunit-filter" ); + + label.innerHTML = "Filter: "; + + input.type = "text"; + input.value = config.filter || ""; + input.name = "filter"; + input.id = "qunit-filter-input"; + + button.innerHTML = "Go"; + + label.appendChild( input ); + + filter.appendChild( label ); + filter.appendChild( button ); + addEvent( filter, "submit", function( ev ) { + applyUrlParams(); + + if ( ev && ev.preventDefault ) { + ev.preventDefault(); + } + + return false; + }); + + return filter; +} + +function toolbarModuleFilterHtml() { + var i, + moduleFilterHtml = ""; + + if ( !modulesList.length ) { + return false; + } + + modulesList.sort(function( a, b ) { + return a.localeCompare( b ); + }); + + moduleFilterHtml += "" + + ""; + + return moduleFilterHtml; +} + +function toolbarModuleFilter() { + var toolbar = id( "qunit-testrunner-toolbar" ), + moduleFilter = document.createElement( "span" ), + moduleFilterHtml = toolbarModuleFilterHtml(); + + if ( !toolbar || !moduleFilterHtml ) { + return false; + } + + moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); + moduleFilter.innerHTML = moduleFilterHtml; + + addEvent( moduleFilter.lastChild, "change", applyUrlParams ); + + toolbar.appendChild( moduleFilter ); +} + +function appendToolbar() { + var toolbar = id( "qunit-testrunner-toolbar" ); + + if ( toolbar ) { + toolbar.appendChild( toolbarUrlConfigContainer() ); + toolbar.appendChild( toolbarLooseFilter() ); + } +} + +function appendHeader() { + var header = id( "qunit-header" ); + + if ( header ) { + header.innerHTML = "" + header.innerHTML + " "; + } +} + +function appendBanner() { + var banner = id( "qunit-banner" ); + + if ( banner ) { + banner.className = ""; + } +} + +function appendTestResults() { + var tests = id( "qunit-tests" ), + result = id( "qunit-testresult" ); + + if ( result ) { + result.parentNode.removeChild( result ); + } + + if ( tests ) { + tests.innerHTML = ""; + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = "Running...
     "; + } +} + +function storeFixture() { + var fixture = id( "qunit-fixture" ); + if ( fixture ) { + config.fixture = fixture.innerHTML; + } +} + +function appendUserAgent() { + var userAgent = id( "qunit-userAgent" ); + + if ( userAgent ) { + userAgent.innerHTML = ""; + userAgent.appendChild( + document.createTextNode( + "QUnit " + QUnit.version + "; " + navigator.userAgent + ) + ); + } +} + +function appendTestsList( modules ) { + var i, l, x, z, test, moduleObj; + + for ( i = 0, l = modules.length; i < l; i++ ) { + moduleObj = modules[ i ]; + + if ( moduleObj.name ) { + modulesList.push( moduleObj.name ); + } + + for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { + test = moduleObj.tests[ x ]; + + appendTest( test.name, test.testId, moduleObj.name ); + } + } +} + +function appendTest( name, testId, moduleName ) { + var title, rerunTrigger, testBlock, assertList, + tests = id( "qunit-tests" ); + + if ( !tests ) { + return; + } + + title = document.createElement( "strong" ); + title.innerHTML = getNameHtml( name, moduleName ); + + rerunTrigger = document.createElement( "a" ); + rerunTrigger.innerHTML = "Rerun"; + rerunTrigger.href = setUrl({ testId: testId }); + + testBlock = document.createElement( "li" ); + testBlock.appendChild( title ); + testBlock.appendChild( rerunTrigger ); + testBlock.id = "qunit-test-output-" + testId; + + assertList = document.createElement( "ol" ); + assertList.className = "qunit-assert-list"; + + testBlock.appendChild( assertList ); + + tests.appendChild( testBlock ); +} + +// HTML Reporter initialization and load +QUnit.begin(function( details ) { + var qunit = id( "qunit" ); + + // Fixture is the only one necessary to run without the #qunit element + storeFixture(); + + if ( qunit ) { + qunit.innerHTML = + "

    " + escapeText( document.title ) + "

    " + + "

    " + + "
    " + + "

    " + + "
      "; + } + + appendHeader(); + appendBanner(); + appendTestResults(); + appendUserAgent(); + appendToolbar(); + appendTestsList( details.modules ); + toolbarModuleFilter(); + + if ( qunit && config.hidepassed ) { + addClass( qunit.lastChild, "hidepass" ); + } +}); + +QUnit.done(function( details ) { + var i, key, + banner = id( "qunit-banner" ), + tests = id( "qunit-tests" ), + html = [ + "Tests completed in ", + details.runtime, + " milliseconds.
      ", + "", + details.passed, + " assertions of ", + details.total, + " passed, ", + details.failed, + " failed." + ].join( "" ); + + if ( banner ) { + banner.className = details.failed ? "qunit-fail" : "qunit-pass"; + } + + if ( tests ) { + id( "qunit-testresult" ).innerHTML = html; + } + + if ( config.altertitle && defined.document && document.title ) { + + // show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8-charset + document.title = [ + ( details.failed ? "\u2716" : "\u2714" ), + document.title.replace( /^[\u2714\u2716] /i, "" ) + ].join( " " ); + } + + // clear own sessionStorage items if all tests passed + if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { + for ( i = 0; i < sessionStorage.length; i++ ) { + key = sessionStorage.key( i++ ); + if ( key.indexOf( "qunit-test-" ) === 0 ) { + sessionStorage.removeItem( key ); + } + } + } + + // scroll back to top to show results + if ( config.scrolltop && window.scrollTo ) { + window.scrollTo( 0, 0 ); + } +}); + +function getNameHtml( name, module ) { + var nameHtml = ""; + + if ( module ) { + nameHtml = "" + escapeText( module ) + ": "; + } + + nameHtml += "" + escapeText( name ) + ""; + + return nameHtml; +} + +QUnit.testStart(function( details ) { + var running, testBlock, bad; + + testBlock = id( "qunit-test-output-" + details.testId ); + if ( testBlock ) { + testBlock.className = "running"; + } else { + + // Report later registered tests + appendTest( details.name, details.testId, details.module ); + } + + running = id( "qunit-testresult" ); + if ( running ) { + bad = QUnit.config.reorder && defined.sessionStorage && + +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name ); + + running.innerHTML = ( bad ? + "Rerunning previously failed test:
      " : + "Running:
      " ) + + getNameHtml( details.name, details.module ); + } + +}); + +QUnit.log(function( details ) { + var assertList, assertLi, + message, expected, actual, + testItem = id( "qunit-test-output-" + details.testId ); + + if ( !testItem ) { + return; + } + + message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); + message = "" + message + ""; + message += "@ " + details.runtime + " ms"; + + // pushFailure doesn't provide details.expected + // when it calls, it's implicit to also not show expected and diff stuff + // Also, we need to check details.expected existence, as it can exist and be undefined + if ( !details.result && hasOwn.call( details, "expected" ) ) { + expected = escapeText( QUnit.dump.parse( details.expected ) ); + actual = escapeText( QUnit.dump.parse( details.actual ) ); + message += ""; + + if ( actual !== expected ) { + message += "" + + ""; + } else { + if ( expected.indexOf( "[object Array]" ) !== -1 || + expected.indexOf( "[object Object]" ) !== -1 ) { + message += ""; + } + } + + if ( details.source ) { + message += ""; + } + + message += "
      Expected:
      " +
      +			expected +
      +			"
      Result:
      " +
      +				actual + "
      Diff:
      " +
      +				QUnit.diff( expected, actual ) + "
      Message: " + + "Diff suppressed as the depth of object is more than current max depth (" + + QUnit.config.maxDepth + ").

      Hint: Use QUnit.dump.maxDepth to " + + " run with a higher max depth or " + + "Rerun without max depth.

      Source:
      " +
      +				escapeText( details.source ) + "
      "; + + // this occours when pushFailure is set and we have an extracted stack trace + } else if ( !details.result && details.source ) { + message += "" + + "" + + "
      Source:
      " +
      +			escapeText( details.source ) + "
      "; + } + + assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; + + assertLi = document.createElement( "li" ); + assertLi.className = details.result ? "pass" : "fail"; + assertLi.innerHTML = message; + assertList.appendChild( assertLi ); +}); + +QUnit.testDone(function( details ) { + var testTitle, time, testItem, assertList, + good, bad, testCounts, skipped, + tests = id( "qunit-tests" ); + + if ( !tests ) { + return; + } + + testItem = id( "qunit-test-output-" + details.testId ); + + assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; + + good = details.passed; + bad = details.failed; + + // store result when possible + if ( config.reorder && defined.sessionStorage ) { + if ( bad ) { + sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); + } else { + sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); + } + } + + if ( bad === 0 ) { + addClass( assertList, "qunit-collapsed" ); + } + + // testItem.firstChild is the test name + testTitle = testItem.firstChild; + + testCounts = bad ? + "" + bad + ", " + "" + good + ", " : + ""; + + testTitle.innerHTML += " (" + testCounts + + details.assertions.length + ")"; + + if ( details.skipped ) { + testItem.className = "skipped"; + skipped = document.createElement( "em" ); + skipped.className = "qunit-skipped-label"; + skipped.innerHTML = "skipped"; + testItem.insertBefore( skipped, testTitle ); + } else { + addEvent( testTitle, "click", function() { + toggleClass( assertList, "qunit-collapsed" ); + }); + + testItem.className = bad ? "fail" : "pass"; + + time = document.createElement( "span" ); + time.className = "runtime"; + time.innerHTML = details.runtime + " ms"; + testItem.insertBefore( time, assertList ); + } +}); + +if ( defined.document ) { + if ( document.readyState === "complete" ) { + QUnit.load(); + } else { + addEvent( window, "load", QUnit.load ); + } +} else { + config.pageLoaded = true; + config.autorun = true; +} + +})(); \ No newline at end of file diff --git a/src/test/java/org/jscookie/test/integration/CookiesEncodingIT.java b/src/test/java/org/jscookie/test/integration/CookiesEncodingIT.java deleted file mode 100644 index a2c5533..0000000 --- a/src/test/java/org/jscookie/test/integration/CookiesEncodingIT.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.jscookie.test.integration; - -import java.net.URL; - -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.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith( Arquillian.class ) -public class CookiesEncodingIT { - @Deployment - public static Archive createDeployment() { - GenericArchive qunitFiles = ShrinkWrap.create( GenericArchive.class ) - .as( ExplodedImporter.class ) - .importDirectory( "src/main/webapp" ) - .as( GenericArchive.class ); - - WebArchive war = ShrinkWrap.create( WebArchive.class ) - .addPackage( "org.jscookie.test.integration" ) - .merge( qunitFiles, "/", Filters.includeAll() ); - - System.out.println( war.toString( true ) ); - - return war; - } - - @RunAsClient - @Test - public void read_qunit_test( @ArquillianResource URL baseURL ) { - System.out.println( "Available at: " + baseURL.toString() ); - Assert.assertTrue( true ); - } -} diff --git a/src/test/java/org/jscookie/test/integration/TestServlet.java b/src/test/java/org/jscookie/test/integration/TestServlet.java deleted file mode 100644 index cd10b75..0000000 --- a/src/test/java/org/jscookie/test/integration/TestServlet.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.jscookie.test.integration; - -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@WebServlet( urlPatterns = "/" ) -public class TestServlet extends HttpServlet { - private static final long serialVersionUID = 1; - @Override - public void doGet( HttpServletRequest request, HttpServletResponse response ) { - // TODO - } -} 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..75455d0 --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java @@ -0,0 +1,86 @@ +package org.jscookie.test.integration.encoding; + +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.jscookie.test.integration.qunit.QUnitPageObject; +import org.jscookie.test.integration.qunit.QUnitResults; +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.htmlunit.HtmlUnitDriver; +import org.openqa.selenium.support.PageFactory; + +import com.gargoylesoftware.htmlunit.BrowserVersion; + +@RunWith( Arquillian.class ) +public class CookiesEncodingIT { + private static boolean DEBUG_JS = false; + + @Deployment + public static Archive createDeployment() { + boolean RECURSIVE_TRUE = true; + + GenericArchive qunitFiles = ShrinkWrap.create( GenericArchive.class ) + .as( ExplodedImporter.class ) + .importDirectory( "src/main/webapp" ) + .as( GenericArchive.class ); + + WebArchive war = ShrinkWrap.create( WebArchive.class ) + .addPackages( RECURSIVE_TRUE, "org.jscookie.test.integration" ) + .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 = createDriver(); + 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(); + + int expectedPasses = results.getTotal(); + int actualPasses = results.getPassed(); + Assert.assertEquals( "should pass all tests", expectedPasses, actualPasses ); + + dispose( driver ); + } + + private WebDriver createDriver() { + if ( DEBUG_JS ) { + return new FirefoxDriver(); + } + + HtmlUnitDriver driver = new HtmlUnitDriver( BrowserVersion.CHROME ); + driver.setJavascriptEnabled( true ); + + return driver; + } + + private void dispose( WebDriver driver ) { + if ( !DEBUG_JS ) { + 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..5f5dfc1 --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingPageObject.java @@ -0,0 +1,21 @@ +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 ) { + driver.navigate().to( baseURL + "encoding" ); + 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..ffc699d --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java @@ -0,0 +1,19 @@ +package org.jscookie.test.integration.encoding; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet( "/encoding" ) +public class EncodingServlet extends HttpServlet { + private static final long serialVersionUID = 1; + @Override + public void doGet( HttpServletRequest request, HttpServletResponse response ) + throws ServletException, IOException { + request.getRequestDispatcher( "/encoding.html" ).forward( request, response ); + } +} 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..a241b64 --- /dev/null +++ b/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java @@ -0,0 +1,49 @@ +package org.jscookie.test.integration.qunit; + +import java.io.IOException; + +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 COMPLETION_TIMEOUT = 60; + private ObjectMapper mapper = new ObjectMapper(); + public QUnitPageObject( WebDriver driver ) { + this.driver = driver; + } + public QUnitResults waitForTests() { + return new WebDriverWait( driver, COMPLETION_TIMEOUT ) + .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)" + ); + System.out.println( "Waiting for 'window.global_test_results': " + result ); + if ( result != 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; + } + } +} From e639cbc3c9de825eae53c6784f994321d89c377d Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sat, 23 May 2015 14:43:21 -0300 Subject: [PATCH 24/63] Add the .project file for eclipse --- .project | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .project 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 + + From 40b48819af2382c3e1ee73e6c0cd0de125d0805c Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sat, 23 May 2015 23:49:39 -0300 Subject: [PATCH 25/63] Add frontend-maven-plugin to run bower inside maven lifecycle --- .gitignore | 2 ++ bower.json | 4 ++++ package.json | 7 +++++++ pom.xml | 23 +++++++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 bower.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index ea8c4bf..f22c522 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +/node +/node_modules \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..d78f68c --- /dev/null +++ b/bower.json @@ -0,0 +1,4 @@ +{ + "name": "java-cookie", + "version": "1.0.0" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3b00060 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "java-cookie", + "version": "1.0.0", + "devDependencies": { + "bower": "1.4.1" + } +} diff --git a/pom.xml b/pom.xml index a47de24..ca62861 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,29 @@ + + 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 + + + + From fd83e19a6de4e587256d3018c44b80cd2ab3c0ef Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 11:41:43 -0300 Subject: [PATCH 26/63] Need to ignore bower_components --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f22c522..2081f21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /node -/node_modules \ No newline at end of file +/node_modules +/bower_components \ No newline at end of file From c03b77e4214e473fa408843c459ab1012e550f95 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 11:42:26 -0300 Subject: [PATCH 27/63] Create a temporary reference to the js-cookie topic branch --- bower.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index d78f68c..6447c61 100644 --- a/bower.json +++ b/bower.json @@ -1,4 +1,7 @@ { "name": "java-cookie", - "version": "1.0.0" + "version": "1.0.0", + "devDependencies": { + "js-cookie": "integration-api" + } } From cf645946f93fb0712392b57e559ee0ea8e4e45ae Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 18:39:47 -0300 Subject: [PATCH 28/63] Add files to run the front-end build --- Gruntfile.js | 18 ++++++++++++++++++ package.json | 9 ++++++++- pom.xml | 6 ++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Gruntfile.js 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/package.json b/package.json index 3b00060..4edcf21 100644 --- a/package.json +++ b/package.json @@ -1,7 +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" + "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 index ca62861..d1ac13f 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,12 @@ bower + + grunt build + + grunt + + From a53a9ce63bc1e922f8289f378e1fa85feed7f4a0 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 19:13:17 -0300 Subject: [PATCH 29/63] Proper hanging if debug is set to true --- .../encoding/CookiesEncodingIT.java | 11 +++++---- .../encoding/EncodingPageObject.java | 4 +++- .../integration/encoding/EncodingServlet.java | 2 +- .../integration/qunit/QUnitPageObject.java | 23 +++++++++++-------- .../test/integration/test/utils/Debug.java | 12 ++++++++++ 5 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 src/test/java/org/jscookie/test/integration/test/utils/Debug.java diff --git a/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java index 75455d0..e14bb53 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java +++ b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java @@ -15,6 +15,7 @@ import org.jboss.shrinkwrap.api.spec.WebArchive; 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; @@ -27,7 +28,7 @@ @RunWith( Arquillian.class ) public class CookiesEncodingIT { - private static boolean DEBUG_JS = false; + private static Debug debug = Debug.FALSE; @Deployment public static Archive createDeployment() { @@ -35,7 +36,7 @@ public static Archive createDeployment() { GenericArchive qunitFiles = ShrinkWrap.create( GenericArchive.class ) .as( ExplodedImporter.class ) - .importDirectory( "src/main/webapp" ) + .importDirectory( "bower_components/js-cookie/" ) .as( GenericArchive.class ); WebArchive war = ShrinkWrap.create( WebArchive.class ) @@ -58,7 +59,7 @@ public void read_qunit_test( @ArquillianResource URL baseURL ) { EncodingPageObject encoding = PageFactory.initElements( driver, EncodingPageObject.class ); encoding.navigateTo( baseURL ); QUnitPageObject qunit = encoding.qunit(); - QUnitResults results = qunit.waitForTests(); + QUnitResults results = qunit.waitForTests( debug ); int expectedPasses = results.getTotal(); int actualPasses = results.getPassed(); @@ -68,7 +69,7 @@ public void read_qunit_test( @ArquillianResource URL baseURL ) { } private WebDriver createDriver() { - if ( DEBUG_JS ) { + if ( debug.is( true ) ) { return new FirefoxDriver(); } @@ -79,7 +80,7 @@ private WebDriver createDriver() { } private void dispose( WebDriver driver ) { - if ( !DEBUG_JS ) { + 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 index 5f5dfc1..9dd399f 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/EncodingPageObject.java +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingPageObject.java @@ -12,7 +12,9 @@ public EncodingPageObject( WebDriver driver ) { this.driver = driver; } EncodingPageObject navigateTo( URL baseURL ) { - driver.navigate().to( baseURL + "encoding" ); + String query = "integration_baseurl=" + baseURL; + String url = baseURL + "test/encoding.html"; + driver.navigate().to( url + "?" + query ); return this; } QUnitPageObject qunit() { diff --git a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java index ffc699d..c5867e6 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java @@ -14,6 +14,6 @@ public class EncodingServlet extends HttpServlet { @Override public void doGet( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException { - request.getRequestDispatcher( "/encoding.html" ).forward( request, response ); + System.out.println( "EXECUTED SERVLET: " + request.getParameter( "name" ) ); } } diff --git a/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java b/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java index a241b64..3ecb552 100644 --- a/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java +++ b/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java @@ -2,6 +2,7 @@ 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; @@ -11,13 +12,13 @@ public class QUnitPageObject { private WebDriver driver; - private static int COMPLETION_TIMEOUT = 60; + private static int TIMEOUT_IN_SECONDS = 1800; private ObjectMapper mapper = new ObjectMapper(); public QUnitPageObject( WebDriver driver ) { this.driver = driver; } - public QUnitResults waitForTests() { - return new WebDriverWait( driver, COMPLETION_TIMEOUT ) + public QUnitResults waitForTests( final Debug debug ) { + return new WebDriverWait( driver, TIMEOUT_IN_SECONDS ) .until(new Function() { @Override public QUnitResults apply( WebDriver input ) { @@ -28,12 +29,16 @@ public QUnitResults apply( WebDriver input ) { "return window.global_test_results && JSON.stringify(window.global_test_results)" ); System.out.println( "Waiting for 'window.global_test_results': " + result ); - if ( result != null ) { - try { - return mapper.readValue( result, QUnitResults.class ); - } catch ( IOException cause ) { - throw new GlobalTestResultException( result, cause ); - } + if ( result == null ) { + return null; + } + if ( debug.is( true ) ) { + return null; + } + try { + return mapper.readValue( result, QUnitResults.class ); + } catch ( IOException cause ) { + throw new GlobalTestResultException( result, cause ); } } return null; 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; + } +} From b761d183f11124b6bcdcda9ecd2969eecdb78bc2 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 19:13:54 -0300 Subject: [PATCH 30/63] These files were just for testing, not needed anymore --- src/main/webapp/encoding.html | 45 - src/main/webapp/qunit-1.18.0.css | 291 --- src/main/webapp/qunit-1.18.0.js | 3828 ------------------------------ 3 files changed, 4164 deletions(-) delete mode 100644 src/main/webapp/encoding.html delete mode 100644 src/main/webapp/qunit-1.18.0.css delete mode 100644 src/main/webapp/qunit-1.18.0.js diff --git a/src/main/webapp/encoding.html b/src/main/webapp/encoding.html deleted file mode 100644 index 66a9e4c..0000000 --- a/src/main/webapp/encoding.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - QUnit Example - - - -
      -
      - - - - \ No newline at end of file diff --git a/src/main/webapp/qunit-1.18.0.css b/src/main/webapp/qunit-1.18.0.css deleted file mode 100644 index 2c407b2..0000000 --- a/src/main/webapp/qunit-1.18.0.css +++ /dev/null @@ -1,291 +0,0 @@ -/*! - * QUnit 1.18.0 - * http://qunitjs.com/ - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2015-04-03T10:23Z - */ - -/** Font Family and Sizes */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { - font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; -} - -#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } -#qunit-tests { font-size: smaller; } - - -/** Resets */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { - margin: 0; - padding: 0; -} - - -/** Header */ - -#qunit-header { - padding: 0.5em 0 0.5em 1em; - - color: #8699A4; - background-color: #0D3349; - - font-size: 1.5em; - line-height: 1em; - font-weight: 400; - - border-radius: 5px 5px 0 0; -} - -#qunit-header a { - text-decoration: none; - color: #C2CCD1; -} - -#qunit-header a:hover, -#qunit-header a:focus { - color: #FFF; -} - -#qunit-testrunner-toolbar label { - display: inline-block; - padding: 0 0.5em 0 0.1em; -} - -#qunit-banner { - height: 5px; -} - -#qunit-testrunner-toolbar { - padding: 0.5em 1em 0.5em 1em; - color: #5E740B; - background-color: #EEE; - overflow: hidden; -} - -#qunit-userAgent { - padding: 0.5em 1em 0.5em 1em; - background-color: #2B81AF; - color: #FFF; - text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; -} - -#qunit-modulefilter-container { - float: right; - padding: 0.2em; -} - -.qunit-url-config { - display: inline-block; - padding: 0.1em; -} - -.qunit-filter { - display: block; - float: right; - margin-left: 1em; -} - -/** Tests: Pass/Fail */ - -#qunit-tests { - list-style-position: inside; -} - -#qunit-tests li { - padding: 0.4em 1em 0.4em 1em; - border-bottom: 1px solid #FFF; - list-style-position: inside; -} - -#qunit-tests > li { - display: none; -} - -#qunit-tests li.running, -#qunit-tests li.pass, -#qunit-tests li.fail, -#qunit-tests li.skipped { - display: list-item; -} - -#qunit-tests.hidepass li.running, -#qunit-tests.hidepass li.pass { - visibility: hidden; - position: absolute; - width: 0px; - height: 0px; - padding: 0; - border: 0; - margin: 0; -} - -#qunit-tests li strong { - cursor: pointer; -} - -#qunit-tests li.skipped strong { - cursor: default; -} - -#qunit-tests li a { - padding: 0.5em; - color: #C2CCD1; - text-decoration: none; -} - -#qunit-tests li p a { - padding: 0.25em; - color: #6B6464; -} -#qunit-tests li a:hover, -#qunit-tests li a:focus { - color: #000; -} - -#qunit-tests li .runtime { - float: right; - font-size: smaller; -} - -.qunit-assert-list { - margin-top: 0.5em; - padding: 0.5em; - - background-color: #FFF; - - border-radius: 5px; -} - -.qunit-collapsed { - display: none; -} - -#qunit-tests table { - border-collapse: collapse; - margin-top: 0.2em; -} - -#qunit-tests th { - text-align: right; - vertical-align: top; - padding: 0 0.5em 0 0; -} - -#qunit-tests td { - vertical-align: top; -} - -#qunit-tests pre { - margin: 0; - white-space: pre-wrap; - word-wrap: break-word; -} - -#qunit-tests del { - background-color: #E0F2BE; - color: #374E0C; - text-decoration: none; -} - -#qunit-tests ins { - background-color: #FFCACA; - color: #500; - text-decoration: none; -} - -/*** Test Counts */ - -#qunit-tests b.counts { color: #000; } -#qunit-tests b.passed { color: #5E740B; } -#qunit-tests b.failed { color: #710909; } - -#qunit-tests li li { - padding: 5px; - background-color: #FFF; - border-bottom: none; - list-style-position: inside; -} - -/*** Passing Styles */ - -#qunit-tests li li.pass { - color: #3C510C; - background-color: #FFF; - border-left: 10px solid #C6E746; -} - -#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } -#qunit-tests .pass .test-name { color: #366097; } - -#qunit-tests .pass .test-actual, -#qunit-tests .pass .test-expected { color: #999; } - -#qunit-banner.qunit-pass { background-color: #C6E746; } - -/*** Failing Styles */ - -#qunit-tests li li.fail { - color: #710909; - background-color: #FFF; - border-left: 10px solid #EE5757; - white-space: pre; -} - -#qunit-tests > li:last-child { - border-radius: 0 0 5px 5px; -} - -#qunit-tests .fail { color: #000; background-color: #EE5757; } -#qunit-tests .fail .test-name, -#qunit-tests .fail .module-name { color: #000; } - -#qunit-tests .fail .test-actual { color: #EE5757; } -#qunit-tests .fail .test-expected { color: #008000; } - -#qunit-banner.qunit-fail { background-color: #EE5757; } - -/*** Skipped tests */ - -#qunit-tests .skipped { - background-color: #EBECE9; -} - -#qunit-tests .qunit-skipped-label { - background-color: #F4FF77; - display: inline-block; - font-style: normal; - color: #366097; - line-height: 1.8em; - padding: 0 0.5em; - margin: -0.4em 0.4em -0.4em 0; -} - -/** Result */ - -#qunit-testresult { - padding: 0.5em 1em 0.5em 1em; - - color: #2B81AF; - background-color: #D2E0E6; - - border-bottom: 1px solid #FFF; -} -#qunit-testresult .module-name { - font-weight: 700; -} - -/** Fixture */ - -#qunit-fixture { - position: absolute; - top: -10000px; - left: -10000px; - width: 1000px; - height: 1000px; -} \ No newline at end of file diff --git a/src/main/webapp/qunit-1.18.0.js b/src/main/webapp/qunit-1.18.0.js deleted file mode 100644 index 4037de2..0000000 --- a/src/main/webapp/qunit-1.18.0.js +++ /dev/null @@ -1,3828 +0,0 @@ -/*! - * QUnit 1.18.0 - * http://qunitjs.com/ - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2015-04-03T10:23Z - */ - -(function( window ) { - -var QUnit, - config, - onErrorFnPrev, - loggingCallbacks = {}, - fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ), - toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty, - // Keep a local reference to Date (GH-283) - Date = window.Date, - now = Date.now || function() { - return new Date().getTime(); - }, - globalStartCalled = false, - runStarted = false, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - defined = { - document: window.document !== undefined, - setTimeout: window.setTimeout !== undefined, - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch ( e ) { - return false; - } - }()) - }, - /** - * Provides a normalized error string, correcting an issue - * with IE 7 (and prior) where Error.prototype.toString is - * not properly implemented - * - * Based on http://es5.github.com/#x15.11.4.4 - * - * @param {String|Error} error - * @return {String} error message - */ - errorString = function( error ) { - var name, message, - errorString = error.toString(); - if ( errorString.substring( 0, 7 ) === "[object" ) { - name = error.name ? error.name.toString() : "Error"; - message = error.message ? error.message.toString() : ""; - if ( name && message ) { - return name + ": " + message; - } else if ( name ) { - return name; - } else if ( message ) { - return message; - } else { - return "Error"; - } - } else { - return errorString; - } - }, - /** - * Makes a clone of an object using only Array or Object as base, - * and copies over the own enumerable properties. - * - * @param {Object} obj - * @return {Object} New object with only the own properties (recursively). - */ - objectValues = function( obj ) { - var key, val, - vals = QUnit.is( "array", obj ) ? [] : {}; - for ( key in obj ) { - if ( hasOwn.call( obj, key ) ) { - val = obj[ key ]; - vals[ key ] = val === Object( val ) ? objectValues( val ) : val; - } - } - return vals; - }; - -QUnit = {}; - -/** - * Config object: Maintain internal state - * Later exposed as QUnit.config - * `config` initialized at top of scope - */ -config = { - // The queue of tests to run - queue: [], - - // block until document ready - blocking: true, - - // by default, run previously failed tests first - // very useful in combination with "Hide passed tests" checked - reorder: true, - - // by default, modify document.title when suite is done - altertitle: true, - - // by default, scroll to top of the page when suite is done - scrolltop: true, - - // when enabled, all tests must call expect() - requireExpects: false, - - // depth up-to which object will be dumped - maxDepth: 5, - - // add checkboxes that are persisted in the query-string - // when enabled, the id is set to `true` as a `QUnit.config` property - urlConfig: [ - { - id: "hidepassed", - label: "Hide passed tests", - tooltip: "Only show tests and assertions that fail. Stored as query-strings." - }, - { - id: "noglobals", - label: "Check for Globals", - tooltip: "Enabling this will test if any test introduces new properties on the " + - "`window` object. Stored as query-strings." - }, - { - id: "notrycatch", - label: "No try-catch", - tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + - "exceptions in IE reasonable. Stored as query-strings." - } - ], - - // Set of all modules. - modules: [], - - // The first unnamed module - currentModule: { - name: "", - tests: [] - }, - - callbacks: {} -}; - -// Push a loose unnamed module to the modules collection -config.modules.push( config.currentModule ); - -// Initialize more QUnit.config and QUnit.urlParams -(function() { - var i, current, - location = window.location || { search: "", protocol: "file:" }, - params = location.search.slice( 1 ).split( "&" ), - length = params.length, - urlParams = {}; - - if ( params[ 0 ] ) { - for ( i = 0; i < length; i++ ) { - current = params[ i ].split( "=" ); - current[ 0 ] = decodeURIComponent( current[ 0 ] ); - - // allow just a key to turn on a flag, e.g., test.html?noglobals - current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; - if ( urlParams[ current[ 0 ] ] ) { - urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); - } else { - urlParams[ current[ 0 ] ] = current[ 1 ]; - } - } - } - - if ( urlParams.filter === true ) { - delete urlParams.filter; - } - - QUnit.urlParams = urlParams; - - // String search anywhere in moduleName+testName - config.filter = urlParams.filter; - - if ( urlParams.maxDepth ) { - config.maxDepth = parseInt( urlParams.maxDepth, 10 ) === -1 ? - Number.POSITIVE_INFINITY : - urlParams.maxDepth; - } - - config.testId = []; - if ( urlParams.testId ) { - - // Ensure that urlParams.testId is an array - urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," ); - for ( i = 0; i < urlParams.testId.length; i++ ) { - config.testId.push( urlParams.testId[ i ] ); - } - } - - // Figure out if we're running the tests from a server or not - QUnit.isLocal = location.protocol === "file:"; - - // Expose the current QUnit version - QUnit.version = "1.18.0"; -}()); - -// Root QUnit object. -// `QUnit` initialized at top of scope -extend( QUnit, { - - // call on start of module test to prepend name to all tests - module: function( name, testEnvironment ) { - var currentModule = { - name: name, - testEnvironment: testEnvironment, - tests: [] - }; - - // DEPRECATED: handles setup/teardown functions, - // beforeEach and afterEach should be used instead - if ( testEnvironment && testEnvironment.setup ) { - testEnvironment.beforeEach = testEnvironment.setup; - delete testEnvironment.setup; - } - if ( testEnvironment && testEnvironment.teardown ) { - testEnvironment.afterEach = testEnvironment.teardown; - delete testEnvironment.teardown; - } - - config.modules.push( currentModule ); - config.currentModule = currentModule; - }, - - // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. - asyncTest: function( testName, expected, callback ) { - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - - QUnit.test( testName, expected, callback, true ); - }, - - test: function( testName, expected, callback, async ) { - var test; - - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - - test = new Test({ - testName: testName, - expected: expected, - async: async, - callback: callback - }); - - test.queue(); - }, - - skip: function( testName ) { - var test = new Test({ - testName: testName, - skip: true - }); - - test.queue(); - }, - - // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0. - // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior. - start: function( count ) { - var globalStartAlreadyCalled = globalStartCalled; - - if ( !config.current ) { - globalStartCalled = true; - - if ( runStarted ) { - throw new Error( "Called start() outside of a test context while already started" ); - } else if ( globalStartAlreadyCalled || count > 1 ) { - throw new Error( "Called start() outside of a test context too many times" ); - } else if ( config.autostart ) { - throw new Error( "Called start() outside of a test context when " + - "QUnit.config.autostart was true" ); - } else if ( !config.pageLoaded ) { - - // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it - config.autostart = true; - return; - } - } else { - - // If a test is running, adjust its semaphore - config.current.semaphore -= count || 1; - - // Don't start until equal number of stop-calls - if ( config.current.semaphore > 0 ) { - return; - } - - // throw an Error if start is called more often than stop - if ( config.current.semaphore < 0 ) { - config.current.semaphore = 0; - - QUnit.pushFailure( - "Called start() while already started (test's semaphore was 0 already)", - sourceFromStacktrace( 2 ) - ); - return; - } - } - - resumeProcessing(); - }, - - // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0. - stop: function( count ) { - - // If there isn't a test running, don't allow QUnit.stop() to be called - if ( !config.current ) { - throw new Error( "Called stop() outside of a test context" ); - } - - // If a test is running, adjust its semaphore - config.current.semaphore += count || 1; - - pauseProcessing(); - }, - - config: config, - - // Safe object type checking - is: function( type, obj ) { - return QUnit.objectType( obj ) === type; - }, - - objectType: function( obj ) { - if ( typeof obj === "undefined" ) { - return "undefined"; - } - - // Consider: typeof null === object - if ( obj === null ) { - return "null"; - } - - var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), - type = match && match[ 1 ] || ""; - - switch ( type ) { - case "Number": - if ( isNaN( obj ) ) { - return "nan"; - } - return "number"; - case "String": - case "Boolean": - case "Array": - case "Date": - case "RegExp": - case "Function": - return type.toLowerCase(); - } - if ( typeof obj === "object" ) { - return "object"; - } - return undefined; - }, - - extend: extend, - - load: function() { - config.pageLoaded = true; - - // Initialize the configuration options - extend( config, { - stats: { all: 0, bad: 0 }, - moduleStats: { all: 0, bad: 0 }, - started: 0, - updateRate: 1000, - autostart: true, - filter: "" - }, true ); - - config.blocking = false; - - if ( config.autostart ) { - resumeProcessing(); - } - } -}); - -// Register logging callbacks -(function() { - var i, l, key, - callbacks = [ "begin", "done", "log", "testStart", "testDone", - "moduleStart", "moduleDone" ]; - - function registerLoggingCallback( key ) { - var loggingCallback = function( callback ) { - if ( QUnit.objectType( callback ) !== "function" ) { - throw new Error( - "QUnit logging methods require a callback function as their first parameters." - ); - } - - config.callbacks[ key ].push( callback ); - }; - - // DEPRECATED: This will be removed on QUnit 2.0.0+ - // Stores the registered functions allowing restoring - // at verifyLoggingCallbacks() if modified - loggingCallbacks[ key ] = loggingCallback; - - return loggingCallback; - } - - for ( i = 0, l = callbacks.length; i < l; i++ ) { - key = callbacks[ i ]; - - // Initialize key collection of logging callback - if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) { - config.callbacks[ key ] = []; - } - - QUnit[ key ] = registerLoggingCallback( key ); - } -})(); - -// `onErrorFnPrev` initialized at top of scope -// Preserve other handlers -onErrorFnPrev = window.onerror; - -// Cover uncaught exceptions -// Returning true will suppress the default browser handler, -// returning false will let it run. -window.onerror = function( error, filePath, linerNr ) { - var ret = false; - if ( onErrorFnPrev ) { - ret = onErrorFnPrev( error, filePath, linerNr ); - } - - // Treat return value as window.onerror itself does, - // Only do our handling if not suppressed. - if ( ret !== true ) { - if ( QUnit.config.current ) { - if ( QUnit.config.current.ignoreGlobalErrors ) { - return true; - } - QUnit.pushFailure( error, filePath + ":" + linerNr ); - } else { - QUnit.test( "global failure", extend(function() { - QUnit.pushFailure( error, filePath + ":" + linerNr ); - }, { validTest: true } ) ); - } - return false; - } - - return ret; -}; - -function done() { - var runtime, passed; - - config.autorun = true; - - // Log the last module results - if ( config.previousModule ) { - runLoggingCallbacks( "moduleDone", { - name: config.previousModule.name, - tests: config.previousModule.tests, - failed: config.moduleStats.bad, - passed: config.moduleStats.all - config.moduleStats.bad, - total: config.moduleStats.all, - runtime: now() - config.moduleStats.started - }); - } - delete config.previousModule; - - runtime = now() - config.started; - passed = config.stats.all - config.stats.bad; - - runLoggingCallbacks( "done", { - failed: config.stats.bad, - passed: passed, - total: config.stats.all, - runtime: runtime - }); -} - -// Doesn't support IE6 to IE9, it will return undefined on these browsers -// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack -function extractStacktrace( e, offset ) { - offset = offset === undefined ? 4 : offset; - - var stack, include, i; - - if ( e.stack ) { - stack = e.stack.split( "\n" ); - if ( /^error$/i.test( stack[ 0 ] ) ) { - stack.shift(); - } - if ( fileName ) { - include = []; - for ( i = offset; i < stack.length; i++ ) { - if ( stack[ i ].indexOf( fileName ) !== -1 ) { - break; - } - include.push( stack[ i ] ); - } - if ( include.length ) { - return include.join( "\n" ); - } - } - return stack[ offset ]; - - // Support: Safari <=6 only - } else if ( e.sourceURL ) { - - // exclude useless self-reference for generated Error objects - if ( /qunit.js$/.test( e.sourceURL ) ) { - return; - } - - // for actual exceptions, this is useful - return e.sourceURL + ":" + e.line; - } -} - -function sourceFromStacktrace( offset ) { - var error = new Error(); - - // Support: Safari <=7 only, IE <=10 - 11 only - // Not all browsers generate the `stack` property for `new Error()`, see also #636 - if ( !error.stack ) { - try { - throw error; - } catch ( err ) { - error = err; - } - } - - return extractStacktrace( error, offset ); -} - -function synchronize( callback, last ) { - if ( QUnit.objectType( callback ) === "array" ) { - while ( callback.length ) { - synchronize( callback.shift() ); - } - return; - } - config.queue.push( callback ); - - if ( config.autorun && !config.blocking ) { - process( last ); - } -} - -function process( last ) { - function next() { - process( last ); - } - var start = now(); - config.depth = ( config.depth || 0 ) + 1; - - while ( config.queue.length && !config.blocking ) { - if ( !defined.setTimeout || config.updateRate <= 0 || - ( ( now() - start ) < config.updateRate ) ) { - if ( config.current ) { - - // Reset async tracking for each phase of the Test lifecycle - config.current.usedAsync = false; - } - config.queue.shift()(); - } else { - setTimeout( next, 13 ); - break; - } - } - config.depth--; - if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { - done(); - } -} - -function begin() { - var i, l, - modulesLog = []; - - // If the test run hasn't officially begun yet - if ( !config.started ) { - - // Record the time of the test run's beginning - config.started = now(); - - verifyLoggingCallbacks(); - - // Delete the loose unnamed module if unused. - if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) { - config.modules.shift(); - } - - // Avoid unnecessary information by not logging modules' test environments - for ( i = 0, l = config.modules.length; i < l; i++ ) { - modulesLog.push({ - name: config.modules[ i ].name, - tests: config.modules[ i ].tests - }); - } - - // The test run is officially beginning now - runLoggingCallbacks( "begin", { - totalTests: Test.count, - modules: modulesLog - }); - } - - config.blocking = false; - process( true ); -} - -function resumeProcessing() { - runStarted = true; - - // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.) - if ( defined.setTimeout ) { - setTimeout(function() { - if ( config.current && config.current.semaphore > 0 ) { - return; - } - if ( config.timeout ) { - clearTimeout( config.timeout ); - } - - begin(); - }, 13 ); - } else { - begin(); - } -} - -function pauseProcessing() { - config.blocking = true; - - if ( config.testTimeout && defined.setTimeout ) { - clearTimeout( config.timeout ); - config.timeout = setTimeout(function() { - if ( config.current ) { - config.current.semaphore = 0; - QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) ); - } else { - throw new Error( "Test timed out" ); - } - resumeProcessing(); - }, config.testTimeout ); - } -} - -function saveGlobal() { - config.pollution = []; - - if ( config.noglobals ) { - for ( var key in window ) { - if ( hasOwn.call( window, key ) ) { - // in Opera sometimes DOM element ids show up here, ignore them - if ( /^qunit-test-output/.test( key ) ) { - continue; - } - config.pollution.push( key ); - } - } - } -} - -function checkPollution() { - var newGlobals, - deletedGlobals, - old = config.pollution; - - saveGlobal(); - - newGlobals = diff( config.pollution, old ); - if ( newGlobals.length > 0 ) { - QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); - } - - deletedGlobals = diff( old, config.pollution ); - if ( deletedGlobals.length > 0 ) { - QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); - } -} - -// returns a new Array with the elements that are in a but not in b -function diff( a, b ) { - var i, j, - result = a.slice(); - - for ( i = 0; i < result.length; i++ ) { - for ( j = 0; j < b.length; j++ ) { - if ( result[ i ] === b[ j ] ) { - result.splice( i, 1 ); - i--; - break; - } - } - } - return result; -} - -function extend( a, b, undefOnly ) { - for ( var prop in b ) { - if ( hasOwn.call( b, prop ) ) { - - // Avoid "Member not found" error in IE8 caused by messing with window.constructor - if ( !( prop === "constructor" && a === window ) ) { - if ( b[ prop ] === undefined ) { - delete a[ prop ]; - } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { - a[ prop ] = b[ prop ]; - } - } - } - } - - return a; -} - -function runLoggingCallbacks( key, args ) { - var i, l, callbacks; - - callbacks = config.callbacks[ key ]; - for ( i = 0, l = callbacks.length; i < l; i++ ) { - callbacks[ i ]( args ); - } -} - -// DEPRECATED: This will be removed on 2.0.0+ -// This function verifies if the loggingCallbacks were modified by the user -// If so, it will restore it, assign the given callback and print a console warning -function verifyLoggingCallbacks() { - var loggingCallback, userCallback; - - for ( loggingCallback in loggingCallbacks ) { - if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) { - - userCallback = QUnit[ loggingCallback ]; - - // Restore the callback function - QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ]; - - // Assign the deprecated given callback - QUnit[ loggingCallback ]( userCallback ); - - if ( window.console && window.console.warn ) { - window.console.warn( - "QUnit." + loggingCallback + " was replaced with a new value.\n" + - "Please, check out the documentation on how to apply logging callbacks.\n" + - "Reference: http://api.qunitjs.com/category/callbacks/" - ); - } - } - } -} - -// from jquery.js -function inArray( elem, array ) { - if ( array.indexOf ) { - return array.indexOf( elem ); - } - - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; - } - } - - return -1; -} - -function Test( settings ) { - var i, l; - - ++Test.count; - - extend( this, settings ); - this.assertions = []; - this.semaphore = 0; - this.usedAsync = false; - this.module = config.currentModule; - this.stack = sourceFromStacktrace( 3 ); - - // Register unique strings - for ( i = 0, l = this.module.tests; i < l.length; i++ ) { - if ( this.module.tests[ i ].name === this.testName ) { - this.testName += " "; - } - } - - this.testId = generateHash( this.module.name, this.testName ); - - this.module.tests.push({ - name: this.testName, - testId: this.testId - }); - - if ( settings.skip ) { - - // Skipped tests will fully ignore any sent callback - this.callback = function() {}; - this.async = false; - this.expected = 0; - } else { - this.assert = new Assert( this ); - } -} - -Test.count = 0; - -Test.prototype = { - before: function() { - if ( - - // Emit moduleStart when we're switching from one module to another - this.module !== config.previousModule || - - // They could be equal (both undefined) but if the previousModule property doesn't - // yet exist it means this is the first test in a suite that isn't wrapped in a - // module, in which case we'll just emit a moduleStart event for 'undefined'. - // Without this, reporters can get testStart before moduleStart which is a problem. - !hasOwn.call( config, "previousModule" ) - ) { - if ( hasOwn.call( config, "previousModule" ) ) { - runLoggingCallbacks( "moduleDone", { - name: config.previousModule.name, - tests: config.previousModule.tests, - failed: config.moduleStats.bad, - passed: config.moduleStats.all - config.moduleStats.bad, - total: config.moduleStats.all, - runtime: now() - config.moduleStats.started - }); - } - config.previousModule = this.module; - config.moduleStats = { all: 0, bad: 0, started: now() }; - runLoggingCallbacks( "moduleStart", { - name: this.module.name, - tests: this.module.tests - }); - } - - config.current = this; - - this.testEnvironment = extend( {}, this.module.testEnvironment ); - delete this.testEnvironment.beforeEach; - delete this.testEnvironment.afterEach; - - this.started = now(); - runLoggingCallbacks( "testStart", { - name: this.testName, - module: this.module.name, - testId: this.testId - }); - - if ( !config.pollution ) { - saveGlobal(); - } - }, - - run: function() { - var promise; - - config.current = this; - - if ( this.async ) { - QUnit.stop(); - } - - this.callbackStarted = now(); - - if ( config.notrycatch ) { - promise = this.callback.call( this.testEnvironment, this.assert ); - this.resolvePromise( promise ); - return; - } - - try { - promise = this.callback.call( this.testEnvironment, this.assert ); - this.resolvePromise( promise ); - } catch ( e ) { - this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + - this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); - - // else next test will carry the responsibility - saveGlobal(); - - // Restart the tests if they're blocking - if ( config.blocking ) { - QUnit.start(); - } - } - }, - - after: function() { - checkPollution(); - }, - - queueHook: function( hook, hookName ) { - var promise, - test = this; - return function runHook() { - config.current = test; - if ( config.notrycatch ) { - promise = hook.call( test.testEnvironment, test.assert ); - test.resolvePromise( promise, hookName ); - return; - } - try { - promise = hook.call( test.testEnvironment, test.assert ); - test.resolvePromise( promise, hookName ); - } catch ( error ) { - test.pushFailure( hookName + " failed on " + test.testName + ": " + - ( error.message || error ), extractStacktrace( error, 0 ) ); - } - }; - }, - - // Currently only used for module level hooks, can be used to add global level ones - hooks: function( handler ) { - var hooks = []; - - // Hooks are ignored on skipped tests - if ( this.skip ) { - return hooks; - } - - if ( this.module.testEnvironment && - QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) { - hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) ); - } - - return hooks; - }, - - finish: function() { - config.current = this; - if ( config.requireExpects && this.expected === null ) { - this.pushFailure( "Expected number of assertions to be defined, but expect() was " + - "not called.", this.stack ); - } else if ( this.expected !== null && this.expected !== this.assertions.length ) { - this.pushFailure( "Expected " + this.expected + " assertions, but " + - this.assertions.length + " were run", this.stack ); - } else if ( this.expected === null && !this.assertions.length ) { - this.pushFailure( "Expected at least one assertion, but none were run - call " + - "expect(0) to accept zero assertions.", this.stack ); - } - - var i, - bad = 0; - - this.runtime = now() - this.started; - config.stats.all += this.assertions.length; - config.moduleStats.all += this.assertions.length; - - for ( i = 0; i < this.assertions.length; i++ ) { - if ( !this.assertions[ i ].result ) { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - - runLoggingCallbacks( "testDone", { - name: this.testName, - module: this.module.name, - skipped: !!this.skip, - failed: bad, - passed: this.assertions.length - bad, - total: this.assertions.length, - runtime: this.runtime, - - // HTML Reporter use - assertions: this.assertions, - testId: this.testId, - - // DEPRECATED: this property will be removed in 2.0.0, use runtime instead - duration: this.runtime - }); - - // QUnit.reset() is deprecated and will be replaced for a new - // fixture reset function on QUnit 2.0/2.1. - // It's still called here for backwards compatibility handling - QUnit.reset(); - - config.current = undefined; - }, - - queue: function() { - var bad, - test = this; - - if ( !this.valid() ) { - return; - } - - function run() { - - // each of these can by async - synchronize([ - function() { - test.before(); - }, - - test.hooks( "beforeEach" ), - - function() { - test.run(); - }, - - test.hooks( "afterEach" ).reverse(), - - function() { - test.after(); - }, - function() { - test.finish(); - } - ]); - } - - // `bad` initialized at top of scope - // defer when previous test run passed, if storage is available - bad = QUnit.config.reorder && defined.sessionStorage && - +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); - - if ( bad ) { - run(); - } else { - synchronize( run, true ); - } - }, - - push: function( result, actual, expected, message ) { - var source, - details = { - module: this.module.name, - name: this.testName, - result: result, - message: message, - actual: actual, - expected: expected, - testId: this.testId, - runtime: now() - this.started - }; - - if ( !result ) { - source = sourceFromStacktrace(); - - if ( source ) { - details.source = source; - } - } - - runLoggingCallbacks( "log", details ); - - this.assertions.push({ - result: !!result, - message: message - }); - }, - - pushFailure: function( message, source, actual ) { - if ( !this instanceof Test ) { - throw new Error( "pushFailure() assertion outside test context, was " + - sourceFromStacktrace( 2 ) ); - } - - var details = { - module: this.module.name, - name: this.testName, - result: false, - message: message || "error", - actual: actual || null, - testId: this.testId, - runtime: now() - this.started - }; - - if ( source ) { - details.source = source; - } - - runLoggingCallbacks( "log", details ); - - this.assertions.push({ - result: false, - message: message - }); - }, - - resolvePromise: function( promise, phase ) { - var then, message, - test = this; - if ( promise != null ) { - then = promise.then; - if ( QUnit.objectType( then ) === "function" ) { - QUnit.stop(); - then.call( - promise, - QUnit.start, - function( error ) { - message = "Promise rejected " + - ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + - " " + test.testName + ": " + ( error.message || error ); - test.pushFailure( message, extractStacktrace( error, 0 ) ); - - // else next test will carry the responsibility - saveGlobal(); - - // Unblock - QUnit.start(); - } - ); - } - } - }, - - valid: function() { - var include, - filter = config.filter && config.filter.toLowerCase(), - module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(), - fullName = ( this.module.name + ": " + this.testName ).toLowerCase(); - - // Internally-generated tests are always valid - if ( this.callback && this.callback.validTest ) { - return true; - } - - if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) { - return false; - } - - if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) { - return false; - } - - if ( !filter ) { - return true; - } - - include = filter.charAt( 0 ) !== "!"; - if ( !include ) { - filter = filter.slice( 1 ); - } - - // If the filter matches, we need to honour include - if ( fullName.indexOf( filter ) !== -1 ) { - return include; - } - - // Otherwise, do the opposite - return !include; - } - -}; - -// Resets the test setup. Useful for tests that modify the DOM. -/* -DEPRECATED: Use multiple tests instead of resetting inside a test. -Use testStart or testDone for custom cleanup. -This method will throw an error in 2.0, and will be removed in 2.1 -*/ -QUnit.reset = function() { - - // Return on non-browser environments - // This is necessary to not break on node tests - if ( typeof window === "undefined" ) { - return; - } - - var fixture = defined.document && document.getElementById && - document.getElementById( "qunit-fixture" ); - - if ( fixture ) { - fixture.innerHTML = config.fixture; - } -}; - -QUnit.pushFailure = function() { - if ( !QUnit.config.current ) { - throw new Error( "pushFailure() assertion outside test context, in " + - sourceFromStacktrace( 2 ) ); - } - - // Gets current test obj - var currentTest = QUnit.config.current; - - return currentTest.pushFailure.apply( currentTest, arguments ); -}; - -// Based on Java's String.hashCode, a simple but not -// rigorously collision resistant hashing function -function generateHash( module, testName ) { - var hex, - i = 0, - hash = 0, - str = module + "\x1C" + testName, - len = str.length; - - for ( ; i < len; i++ ) { - hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); - hash |= 0; - } - - // Convert the possibly negative integer hash code into an 8 character hex string, which isn't - // strictly necessary but increases user understanding that the id is a SHA-like hash - hex = ( 0x100000000 + hash ).toString( 16 ); - if ( hex.length < 8 ) { - hex = "0000000" + hex; - } - - return hex.slice( -8 ); -} - -function Assert( testContext ) { - this.test = testContext; -} - -// Assert helpers -QUnit.assert = Assert.prototype = { - - // Specify the number of expected assertions to guarantee that failed test - // (no assertions are run at all) don't slip through. - expect: function( asserts ) { - if ( arguments.length === 1 ) { - this.test.expected = asserts; - } else { - return this.test.expected; - } - }, - - // Increment this Test's semaphore counter, then return a single-use function that - // decrements that counter a maximum of once. - async: function() { - var test = this.test, - popped = false; - - test.semaphore += 1; - test.usedAsync = true; - pauseProcessing(); - - return function done() { - if ( !popped ) { - test.semaphore -= 1; - popped = true; - resumeProcessing(); - } else { - test.pushFailure( "Called the callback returned from `assert.async` more than once", - sourceFromStacktrace( 2 ) ); - } - }; - }, - - // Exports test.push() to the user API - push: function( /* result, actual, expected, message */ ) { - var assert = this, - currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; - - // Backwards compatibility fix. - // Allows the direct use of global exported assertions and QUnit.assert.* - // Although, it's use is not recommended as it can leak assertions - // to other tests from async tests, because we only get a reference to the current test, - // not exactly the test where assertion were intended to be called. - if ( !currentTest ) { - throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); - } - - if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { - currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", - sourceFromStacktrace( 2 ) ); - - // Allow this assertion to continue running anyway... - } - - if ( !( assert instanceof Assert ) ) { - assert = currentTest.assert; - } - return assert.test.push.apply( assert.test, arguments ); - }, - - ok: function( result, message ) { - message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + - QUnit.dump.parse( result ) ); - this.push( !!result, result, true, message ); - }, - - notOk: function( result, message ) { - message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " + - QUnit.dump.parse( result ) ); - this.push( !result, result, false, message ); - }, - - equal: function( actual, expected, message ) { - /*jshint eqeqeq:false */ - this.push( expected == actual, actual, expected, message ); - }, - - notEqual: function( actual, expected, message ) { - /*jshint eqeqeq:false */ - this.push( expected != actual, actual, expected, message ); - }, - - propEqual: function( actual, expected, message ) { - actual = objectValues( actual ); - expected = objectValues( expected ); - this.push( QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - notPropEqual: function( actual, expected, message ) { - actual = objectValues( actual ); - expected = objectValues( expected ); - this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - deepEqual: function( actual, expected, message ) { - this.push( QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - notDeepEqual: function( actual, expected, message ) { - this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - strictEqual: function( actual, expected, message ) { - this.push( expected === actual, actual, expected, message ); - }, - - notStrictEqual: function( actual, expected, message ) { - this.push( expected !== actual, actual, expected, message ); - }, - - "throws": function( block, expected, message ) { - var actual, expectedType, - expectedOutput = expected, - ok = false, - currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current; - - // 'expected' is optional unless doing string comparison - if ( message == null && typeof expected === "string" ) { - message = expected; - expected = null; - } - - currentTest.ignoreGlobalErrors = true; - try { - block.call( currentTest.testEnvironment ); - } catch (e) { - actual = e; - } - currentTest.ignoreGlobalErrors = false; - - if ( actual ) { - expectedType = QUnit.objectType( expected ); - - // we don't want to validate thrown error - if ( !expected ) { - ok = true; - expectedOutput = null; - - // expected is a regexp - } else if ( expectedType === "regexp" ) { - ok = expected.test( errorString( actual ) ); - - // expected is a string - } else if ( expectedType === "string" ) { - ok = expected === errorString( actual ); - - // expected is a constructor, maybe an Error constructor - } else if ( expectedType === "function" && actual instanceof expected ) { - ok = true; - - // expected is an Error object - } else if ( expectedType === "object" ) { - ok = actual instanceof expected.constructor && - actual.name === expected.name && - actual.message === expected.message; - - // expected is a validation function which returns true if validation passed - } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { - expectedOutput = null; - ok = true; - } - } - - currentTest.assert.push( ok, actual, expectedOutput, message ); - } -}; - -// Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word -// Known to us are: Closure Compiler, Narwhal -(function() { - /*jshint sub:true */ - Assert.prototype.raises = Assert.prototype[ "throws" ]; -}()); - -// Test for equality any JavaScript type. -// Author: Philippe Rathé -QUnit.equiv = (function() { - - // Call the o related callback with the given arguments. - function bindCallbacks( o, callbacks, args ) { - var prop = QUnit.objectType( o ); - if ( prop ) { - if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { - return callbacks[ prop ].apply( callbacks, args ); - } else { - return callbacks[ prop ]; // or undefined - } - } - } - - // the real equiv function - var innerEquiv, - - // stack to decide between skip/abort functions - callers = [], - - // stack to avoiding loops from circular referencing - parents = [], - parentsB = [], - - getProto = Object.getPrototypeOf || function( obj ) { - /* jshint camelcase: false, proto: true */ - return obj.__proto__; - }, - callbacks = (function() { - - // for string, boolean, number and null - function useStrictEquality( b, a ) { - - /*jshint eqeqeq:false */ - if ( b instanceof a.constructor || a instanceof b.constructor ) { - - // to catch short annotation VS 'new' annotation of a - // declaration - // e.g. var i = 1; - // var j = new Number(1); - return a == b; - } else { - return a === b; - } - } - - return { - "string": useStrictEquality, - "boolean": useStrictEquality, - "number": useStrictEquality, - "null": useStrictEquality, - "undefined": useStrictEquality, - - "nan": function( b ) { - return isNaN( b ); - }, - - "date": function( b, a ) { - return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); - }, - - "regexp": function( b, a ) { - return QUnit.objectType( b ) === "regexp" && - - // the regex itself - a.source === b.source && - - // and its modifiers - a.global === b.global && - - // (gmi) ... - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline && - a.sticky === b.sticky; - }, - - // - skip when the property is a method of an instance (OOP) - // - abort otherwise, - // initial === would have catch identical references anyway - "function": function() { - var caller = callers[ callers.length - 1 ]; - return caller !== Object && typeof caller !== "undefined"; - }, - - "array": function( b, a ) { - var i, j, len, loop, aCircular, bCircular; - - // b could be an object literal here - if ( QUnit.objectType( b ) !== "array" ) { - return false; - } - - len = a.length; - if ( len !== b.length ) { - // safe and faster - return false; - } - - // track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - for ( i = 0; i < len; i++ ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[ j ] === a[ i ]; - bCircular = parentsB[ j ] === b[ i ]; - if ( aCircular || bCircular ) { - if ( a[ i ] === b[ i ] || aCircular && bCircular ) { - loop = true; - } else { - parents.pop(); - parentsB.pop(); - return false; - } - } - } - if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { - parents.pop(); - parentsB.pop(); - return false; - } - } - parents.pop(); - parentsB.pop(); - return true; - }, - - "object": function( b, a ) { - - /*jshint forin:false */ - var i, j, loop, aCircular, bCircular, - // Default to true - eq = true, - aProperties = [], - bProperties = []; - - // comparing constructors is more strict than using - // instanceof - if ( a.constructor !== b.constructor ) { - - // Allow objects with no prototype to be equivalent to - // objects with Object as their constructor. - if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) || - ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) { - return false; - } - } - - // stack constructor before traversing properties - callers.push( a.constructor ); - - // track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - - // be strict: don't ensure hasOwnProperty and go deep - for ( i in a ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[ j ] === a[ i ]; - bCircular = parentsB[ j ] === b[ i ]; - if ( aCircular || bCircular ) { - if ( a[ i ] === b[ i ] || aCircular && bCircular ) { - loop = true; - } else { - eq = false; - break; - } - } - } - aProperties.push( i ); - if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { - eq = false; - break; - } - } - - parents.pop(); - parentsB.pop(); - callers.pop(); // unstack, we are done - - for ( i in b ) { - bProperties.push( i ); // collect b's properties - } - - // Ensures identical properties name - return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); - } - }; - }()); - - innerEquiv = function() { // can take multiple arguments - var args = [].slice.apply( arguments ); - if ( args.length < 2 ) { - return true; // end transition - } - - return ( (function( a, b ) { - if ( a === b ) { - return true; // catch the most you can - } else if ( a === null || b === null || typeof a === "undefined" || - typeof b === "undefined" || - QUnit.objectType( a ) !== QUnit.objectType( b ) ) { - - // don't lose time with error prone cases - return false; - } else { - return bindCallbacks( a, callbacks, [ b, a ] ); - } - - // apply transition with (1..n) arguments - }( args[ 0 ], args[ 1 ] ) ) && - innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); - }; - - return innerEquiv; -}()); - -// Based on jsDump by Ariel Flesler -// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html -QUnit.dump = (function() { - function quote( str ) { - return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; - } - function literal( o ) { - return o + ""; - } - function join( pre, arr, post ) { - var s = dump.separator(), - base = dump.indent(), - inner = dump.indent( 1 ); - if ( arr.join ) { - arr = arr.join( "," + s + inner ); - } - if ( !arr ) { - return pre + post; - } - return [ pre, inner + arr, base + post ].join( s ); - } - function array( arr, stack ) { - var i = arr.length, - ret = new Array( i ); - - if ( dump.maxDepth && dump.depth > dump.maxDepth ) { - return "[object Array]"; - } - - this.up(); - while ( i-- ) { - ret[ i ] = this.parse( arr[ i ], undefined, stack ); - } - this.down(); - return join( "[", ret, "]" ); - } - - var reName = /^function (\w+)/, - dump = { - - // objType is used mostly internally, you can fix a (custom) type in advance - parse: function( obj, objType, stack ) { - stack = stack || []; - var res, parser, parserType, - inStack = inArray( obj, stack ); - - if ( inStack !== -1 ) { - return "recursion(" + ( inStack - stack.length ) + ")"; - } - - objType = objType || this.typeOf( obj ); - parser = this.parsers[ objType ]; - parserType = typeof parser; - - if ( parserType === "function" ) { - stack.push( obj ); - res = parser.call( this, obj, stack ); - stack.pop(); - return res; - } - return ( parserType === "string" ) ? parser : this.parsers.error; - }, - typeOf: function( obj ) { - var type; - if ( obj === null ) { - type = "null"; - } else if ( typeof obj === "undefined" ) { - type = "undefined"; - } else if ( QUnit.is( "regexp", obj ) ) { - type = "regexp"; - } else if ( QUnit.is( "date", obj ) ) { - type = "date"; - } else if ( QUnit.is( "function", obj ) ) { - type = "function"; - } else if ( obj.setInterval !== undefined && - obj.document !== undefined && - obj.nodeType === undefined ) { - type = "window"; - } else if ( obj.nodeType === 9 ) { - type = "document"; - } else if ( obj.nodeType ) { - type = "node"; - } else if ( - - // native arrays - toString.call( obj ) === "[object Array]" || - - // NodeList objects - ( typeof obj.length === "number" && obj.item !== undefined && - ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && - obj[ 0 ] === undefined ) ) ) - ) { - type = "array"; - } else if ( obj.constructor === Error.prototype.constructor ) { - type = "error"; - } else { - type = typeof obj; - } - return type; - }, - separator: function() { - return this.multiline ? this.HTML ? "
      " : "\n" : this.HTML ? " " : " "; - }, - // extra can be a number, shortcut for increasing-calling-decreasing - indent: function( extra ) { - if ( !this.multiline ) { - return ""; - } - var chr = this.indentChar; - if ( this.HTML ) { - chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); - } - return new Array( this.depth + ( extra || 0 ) ).join( chr ); - }, - up: function( a ) { - this.depth += a || 1; - }, - down: function( a ) { - this.depth -= a || 1; - }, - setParser: function( name, parser ) { - this.parsers[ name ] = parser; - }, - // The next 3 are exposed so you can use them - quote: quote, - literal: literal, - join: join, - // - depth: 1, - maxDepth: QUnit.config.maxDepth, - - // This is the list of parsers, to modify them, use dump.setParser - parsers: { - window: "[Window]", - document: "[Document]", - error: function( error ) { - return "Error(\"" + error.message + "\")"; - }, - unknown: "[Unknown]", - "null": "null", - "undefined": "undefined", - "function": function( fn ) { - var ret = "function", - - // functions never have name in IE - name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; - - if ( name ) { - ret += " " + name; - } - ret += "( "; - - ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); - return join( ret, dump.parse( fn, "functionCode" ), "}" ); - }, - array: array, - nodelist: array, - "arguments": array, - object: function( map, stack ) { - var keys, key, val, i, nonEnumerableProperties, - ret = []; - - if ( dump.maxDepth && dump.depth > dump.maxDepth ) { - return "[object Object]"; - } - - dump.up(); - keys = []; - for ( key in map ) { - keys.push( key ); - } - - // Some properties are not always enumerable on Error objects. - nonEnumerableProperties = [ "message", "name" ]; - for ( i in nonEnumerableProperties ) { - key = nonEnumerableProperties[ i ]; - if ( key in map && inArray( key, keys ) < 0 ) { - keys.push( key ); - } - } - keys.sort(); - for ( i = 0; i < keys.length; i++ ) { - key = keys[ i ]; - val = map[ key ]; - ret.push( dump.parse( key, "key" ) + ": " + - dump.parse( val, undefined, stack ) ); - } - dump.down(); - return join( "{", ret, "}" ); - }, - node: function( node ) { - var len, i, val, - open = dump.HTML ? "<" : "<", - close = dump.HTML ? ">" : ">", - tag = node.nodeName.toLowerCase(), - ret = open + tag, - attrs = node.attributes; - - if ( attrs ) { - for ( i = 0, len = attrs.length; i < len; i++ ) { - val = attrs[ i ].nodeValue; - - // IE6 includes all attributes in .attributes, even ones not explicitly - // set. Those have values like undefined, null, 0, false, "" or - // "inherit". - if ( val && val !== "inherit" ) { - ret += " " + attrs[ i ].nodeName + "=" + - dump.parse( val, "attribute" ); - } - } - } - ret += close; - - // Show content of TextNode or CDATASection - if ( node.nodeType === 3 || node.nodeType === 4 ) { - ret += node.nodeValue; - } - - return ret + open + "/" + tag + close; - }, - - // function calls it internally, it's the arguments part of the function - functionArgs: function( fn ) { - var args, - l = fn.length; - - if ( !l ) { - return ""; - } - - args = new Array( l ); - while ( l-- ) { - - // 97 is 'a' - args[ l ] = String.fromCharCode( 97 + l ); - } - return " " + args.join( ", " ) + " "; - }, - // object calls it internally, the key part of an item in a map - key: quote, - // function calls it internally, it's the content of the function - functionCode: "[code]", - // node calls it internally, it's an html attribute value - attribute: quote, - string: quote, - date: quote, - regexp: literal, - number: literal, - "boolean": literal - }, - // if true, entities are escaped ( <, >, \t, space and \n ) - HTML: false, - // indentation unit - indentChar: " ", - // if true, items in a collection, are separated by a \n, else just a space. - multiline: true - }; - - return dump; -}()); - -// back compat -QUnit.jsDump = QUnit.dump; - -// For browser, export only select globals -if ( typeof window !== "undefined" ) { - - // Deprecated - // Extend assert methods to QUnit and Global scope through Backwards compatibility - (function() { - var i, - assertions = Assert.prototype; - - function applyCurrent( current ) { - return function() { - var assert = new Assert( QUnit.config.current ); - current.apply( assert, arguments ); - }; - } - - for ( i in assertions ) { - QUnit[ i ] = applyCurrent( assertions[ i ] ); - } - })(); - - (function() { - var i, l, - keys = [ - "test", - "module", - "expect", - "asyncTest", - "start", - "stop", - "ok", - "notOk", - "equal", - "notEqual", - "propEqual", - "notPropEqual", - "deepEqual", - "notDeepEqual", - "strictEqual", - "notStrictEqual", - "throws" - ]; - - for ( i = 0, l = keys.length; i < l; i++ ) { - window[ keys[ i ] ] = QUnit[ keys[ i ] ]; - } - })(); - - window.QUnit = QUnit; -} - -// For nodejs -if ( typeof module !== "undefined" && module && module.exports ) { - module.exports = QUnit; - - // For consistency with CommonJS environments' exports - module.exports.QUnit = QUnit; -} - -// For CommonJS with exports, but without module.exports, like Rhino -if ( typeof exports !== "undefined" && exports ) { - exports.QUnit = QUnit; -} - -if ( typeof define === "function" && define.amd ) { - define( function() { - return QUnit; - } ); - QUnit.config.autostart = false; -} - -// Get a reference to the global object, like window in browsers -}( (function() { - return this; -})() )); - -/*istanbul ignore next */ -// jscs:disable maximumLineLength -/* - * This file is a modified version of google-diff-match-patch's JavaScript implementation - * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), - * modifications are licensed as more fully set forth in LICENSE.txt. - * - * The original source of google-diff-match-patch is attributable and licensed as follows: - * - * Copyright 2006 Google Inc. - * http://code.google.com/p/google-diff-match-patch/ - * - * Licensed 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. - * - * More Info: - * https://code.google.com/p/google-diff-match-patch/ - * - * Usage: QUnit.diff(expected, actual) - * - * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) === "the quick brown fox jumpsed} Array of diff tuples. - */ - DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines, optDeadline ) { - var deadline, checklines, commonlength, - commonprefix, commonsuffix, diffs; - // Set a deadline by which time the diff must be complete. - if ( typeof optDeadline === "undefined" ) { - if ( this.DiffTimeout <= 0 ) { - optDeadline = Number.MAX_VALUE; - } else { - optDeadline = ( new Date() ).getTime() + this.DiffTimeout * 1000; - } - } - deadline = optDeadline; - - // Check for null inputs. - if ( text1 === null || text2 === null ) { - throw new Error( "Null input. (DiffMain)" ); - } - - // Check for equality (speedup). - if ( text1 === text2 ) { - if ( text1 ) { - return [ - [ DIFF_EQUAL, text1 ] - ]; - } - return []; - } - - if ( typeof optChecklines === "undefined" ) { - optChecklines = true; - } - - checklines = optChecklines; - - // Trim off common prefix (speedup). - commonlength = this.diffCommonPrefix( text1, text2 ); - commonprefix = text1.substring( 0, commonlength ); - text1 = text1.substring( commonlength ); - text2 = text2.substring( commonlength ); - - // Trim off common suffix (speedup). - ///////// - commonlength = this.diffCommonSuffix( text1, text2 ); - commonsuffix = text1.substring( text1.length - commonlength ); - text1 = text1.substring( 0, text1.length - commonlength ); - text2 = text2.substring( 0, text2.length - commonlength ); - - // Compute the diff on the middle block. - diffs = this.diffCompute( text1, text2, checklines, deadline ); - - // Restore the prefix and suffix. - if ( commonprefix ) { - diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); - } - if ( commonsuffix ) { - diffs.push( [ DIFF_EQUAL, commonsuffix ] ); - } - this.diffCleanupMerge( diffs ); - return diffs; - }; - - /** - * Reduce the number of edits by eliminating operationally trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { - var changes, equalities, equalitiesLength, lastequality, - pointer, preIns, preDel, postIns, postDel; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - // Is there an insertion operation before the last equality. - preIns = false; - // Is there a deletion operation before the last equality. - preDel = false; - // Is there an insertion operation after the last equality. - postIns = false; - // Is there a deletion operation after the last equality. - postDel = false; - while ( pointer < diffs.length ) { - if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. - if ( diffs[ pointer ][ 1 ].length < this.DiffEditCost && ( postIns || postDel ) ) { - // Candidate found. - equalities[ equalitiesLength++ ] = pointer; - preIns = postIns; - preDel = postDel; - lastequality = diffs[ pointer ][ 1 ]; - } else { - // Not a candidate, and can never become one. - equalitiesLength = 0; - lastequality = null; - } - postIns = postDel = false; - } else { // An insertion or deletion. - if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { - postDel = true; - } else { - postIns = true; - } - /* - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - */ - if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || - ( ( lastequality.length < this.DiffEditCost / 2 ) && - ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { - // Duplicate record. - diffs.splice( equalities[equalitiesLength - 1], 0, [ DIFF_DELETE, lastequality ] ); - // Change second copy to insert. - diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; - equalitiesLength--; // Throw away the equality we just deleted; - lastequality = null; - if (preIns && preDel) { - // No changes made which could affect previous entry, keep going. - postIns = postDel = true; - equalitiesLength = 0; - } else { - equalitiesLength--; // Throw away the previous equality. - pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; - postIns = postDel = false; - } - changes = true; - } - } - pointer++; - } - - if ( changes ) { - this.diffCleanupMerge( diffs ); - } - }; - - /** - * Convert a diff array into a pretty HTML report. - * @param {!Array.} diffs Array of diff tuples. - * @param {integer} string to be beautified. - * @return {string} HTML representation. - */ - DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { - var op, data, x, html = []; - for ( x = 0; x < diffs.length; x++ ) { - op = diffs[x][0]; // Operation (insert, delete, equal) - data = diffs[x][1]; // Text of change. - switch ( op ) { - case DIFF_INSERT: - html[x] = "" + data + ""; - break; - case DIFF_DELETE: - html[x] = "" + data + ""; - break; - case DIFF_EQUAL: - html[x] = "" + data + ""; - break; - } - } - return html.join(""); - }; - - /** - * Determine the common prefix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the start of each - * string. - */ - DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { - var pointermid, pointermax, pointermin, pointerstart; - // Quick check for common null cases. - if ( !text1 || !text2 || text1.charAt(0) !== text2.charAt(0) ) { - return 0; - } - // Binary search. - // Performance analysis: http://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min( text1.length, text2.length ); - pointermid = pointermax; - pointerstart = 0; - while ( pointermin < pointermid ) { - if ( text1.substring( pointerstart, pointermid ) === text2.substring( pointerstart, pointermid ) ) { - pointermin = pointermid; - pointerstart = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); - } - return pointermid; - }; - - /** - * Determine the common suffix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of each string. - */ - DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { - var pointermid, pointermax, pointermin, pointerend; - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { - return 0; - } - // Binary search. - // Performance analysis: http://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min(text1.length, text2.length); - pointermid = pointermax; - pointerend = 0; - while ( pointermin < pointermid ) { - if (text1.substring( text1.length - pointermid, text1.length - pointerend ) === - text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { - pointermin = pointermid; - pointerend = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); - } - return pointermid; - }; - - /** - * Find the differences between two texts. Assumes that the texts do not - * have any common prefix or suffix. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean} checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster, slightly less optimal diff. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { - var diffs, longtext, shorttext, i, hm, - text1A, text2A, text1B, text2B, - midCommon, diffsA, diffsB; - - if ( !text1 ) { - // Just add some text (speedup). - return [ - [ DIFF_INSERT, text2 ] - ]; - } - - if (!text2) { - // Just delete some text (speedup). - return [ - [ DIFF_DELETE, text1 ] - ]; - } - - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - i = longtext.indexOf( shorttext ); - if ( i !== -1 ) { - // Shorter text is inside the longer text (speedup). - diffs = [ - [ DIFF_INSERT, longtext.substring( 0, i ) ], - [ DIFF_EQUAL, shorttext ], - [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] - ]; - // Swap insertions for deletions if diff is reversed. - if ( text1.length > text2.length ) { - diffs[0][0] = diffs[2][0] = DIFF_DELETE; - } - return diffs; - } - - if ( shorttext.length === 1 ) { - // Single character string. - // After the previous speedup, the character can't be an equality. - return [ - [ DIFF_DELETE, text1 ], - [ DIFF_INSERT, text2 ] - ]; - } - - // Check to see if the problem can be split in two. - hm = this.diffHalfMatch(text1, text2); - if (hm) { - // A half-match was found, sort out the return data. - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - midCommon = hm[4]; - // Send both pairs off for separate processing. - diffsA = this.DiffMain(text1A, text2A, checklines, deadline); - diffsB = this.DiffMain(text1B, text2B, checklines, deadline); - // Merge the results. - return diffsA.concat([ - [ DIFF_EQUAL, midCommon ] - ], diffsB); - } - - if (checklines && text1.length > 100 && text2.length > 100) { - return this.diffLineMode(text1, text2, deadline); - } - - return this.diffBisect(text1, text2, deadline); - }; - - /** - * Do the two texts share a substring which is at least half the length of the - * longer text? - * This speedup can produce non-minimal diffs. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {Array.} Five element Array, containing the prefix of - * text1, the suffix of text1, the prefix of text2, the suffix of - * text2 and the common middle. Or null if there was no match. - * @private - */ - DiffMatchPatch.prototype.diffHalfMatch = function(text1, text2) { - var longtext, shorttext, dmp, - text1A, text2B, text2A, text1B, midCommon, - hm1, hm2, hm; - if (this.DiffTimeout <= 0) { - // Don't risk returning a non-optimal diff if we have unlimited time. - return null; - } - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { - return null; // Pointless. - } - dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Does a substring of shorttext exist within longtext such that the substring - * is at least half the length of longtext? - * Closure, but does not reference any external variables. - * @param {string} longtext Longer string. - * @param {string} shorttext Shorter string. - * @param {number} i Start index of quarter length substring within longtext. - * @return {Array.} Five element Array, containing the prefix of - * longtext, the suffix of longtext, the prefix of shorttext, the suffix - * of shorttext and the common middle. Or null if there was no match. - * @private - */ - function diffHalfMatchI(longtext, shorttext, i) { - var seed, j, bestCommon, prefixLength, suffixLength, - bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; - // Start with a 1/4 length substring at position i as a seed. - seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); - j = -1; - bestCommon = ""; - while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { - prefixLength = dmp.diffCommonPrefix(longtext.substring(i), - shorttext.substring(j)); - suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), - shorttext.substring(0, j)); - if (bestCommon.length < suffixLength + prefixLength) { - bestCommon = shorttext.substring(j - suffixLength, j) + - shorttext.substring(j, j + prefixLength); - bestLongtextA = longtext.substring(0, i - suffixLength); - bestLongtextB = longtext.substring(i + prefixLength); - bestShorttextA = shorttext.substring(0, j - suffixLength); - bestShorttextB = shorttext.substring(j + prefixLength); - } - } - if (bestCommon.length * 2 >= longtext.length) { - return [ bestLongtextA, bestLongtextB, - bestShorttextA, bestShorttextB, bestCommon - ]; - } else { - return null; - } - } - - // First check if the second quarter is the seed for a half-match. - hm1 = diffHalfMatchI(longtext, shorttext, - Math.ceil(longtext.length / 4)); - // Check again based on the third quarter. - hm2 = diffHalfMatchI(longtext, shorttext, - Math.ceil(longtext.length / 2)); - if (!hm1 && !hm2) { - return null; - } else if (!hm2) { - hm = hm1; - } else if (!hm1) { - hm = hm2; - } else { - // Both matched. Select the longest. - hm = hm1[4].length > hm2[4].length ? hm1 : hm2; - } - - // A half-match was found, sort out the return data. - text1A, text1B, text2A, text2B; - if (text1.length > text2.length) { - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - } else { - text2A = hm[0]; - text2B = hm[1]; - text1A = hm[2]; - text1B = hm[3]; - } - midCommon = hm[4]; - return [ text1A, text1B, text2A, text2B, midCommon ]; - }; - - /** - * Do a quick line-level diff on both strings, then rediff the parts for - * greater accuracy. - * This speedup can produce non-minimal diffs. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffLineMode = function(text1, text2, deadline) { - var a, diffs, linearray, pointer, countInsert, - countDelete, textInsert, textDelete, j; - // Scan the text on a line-by-line basis first. - a = this.diffLinesToChars(text1, text2); - text1 = a.chars1; - text2 = a.chars2; - linearray = a.lineArray; - - diffs = this.DiffMain(text1, text2, false, deadline); - - // Convert the diff back to original text. - this.diffCharsToLines(diffs, linearray); - // Eliminate freak matches (e.g. blank lines) - this.diffCleanupSemantic(diffs); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.push( [ DIFF_EQUAL, "" ] ); - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - while (pointer < diffs.length) { - switch ( diffs[pointer][0] ) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (countDelete >= 1 && countInsert >= 1) { - // Delete the offending records and add the merged ones. - diffs.splice(pointer - countDelete - countInsert, - countDelete + countInsert); - pointer = pointer - countDelete - countInsert; - a = this.DiffMain(textDelete, textInsert, false, deadline); - for (j = a.length - 1; j >= 0; j--) { - diffs.splice( pointer, 0, a[j] ); - } - pointer = pointer + a.length; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - pointer++; - } - diffs.pop(); // Remove the dummy entry at the end. - - return diffs; - }; - - /** - * Find the 'middle snake' of a diff, split the problem in two - * and return the recursively constructed diff. - * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisect = function(text1, text2, deadline) { - var text1Length, text2Length, maxD, vOffset, vLength, - v1, v2, x, delta, front, k1start, k1end, k2start, - k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - maxD = Math.ceil((text1Length + text2Length) / 2); - vOffset = maxD; - vLength = 2 * maxD; - v1 = new Array(vLength); - v2 = new Array(vLength); - // Setting all elements to -1 is faster in Chrome & Firefox than mixing - // integers and undefined. - for (x = 0; x < vLength; x++) { - v1[x] = -1; - v2[x] = -1; - } - v1[vOffset + 1] = 0; - v2[vOffset + 1] = 0; - delta = text1Length - text2Length; - // If the total number of characters is odd, then the front path will collide - // with the reverse path. - front = (delta % 2 !== 0); - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - k1start = 0; - k1end = 0; - k2start = 0; - k2end = 0; - for (d = 0; d < maxD; d++) { - // Bail out if deadline is reached. - if ((new Date()).getTime() > deadline) { - break; - } - - // Walk the front path one step. - for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { - k1Offset = vOffset + k1; - if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { - x1 = v1[k1Offset + 1]; - } else { - x1 = v1[k1Offset - 1] + 1; - } - y1 = x1 - k1; - while (x1 < text1Length && y1 < text2Length && - text1.charAt(x1) === text2.charAt(y1)) { - x1++; - y1++; - } - v1[k1Offset] = x1; - if (x1 > text1Length) { - // Ran off the right of the graph. - k1end += 2; - } else if (y1 > text2Length) { - // Ran off the bottom of the graph. - k1start += 2; - } else if (front) { - k2Offset = vOffset + delta - k1; - if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - v2[k2Offset]; - if (x1 >= x2) { - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - - // Walk the reverse path one step. - for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { - k2Offset = vOffset + k2; - if ( k2 === -d || (k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { - x2 = v2[k2Offset + 1]; - } else { - x2 = v2[k2Offset - 1] + 1; - } - y2 = x2 - k2; - while (x2 < text1Length && y2 < text2Length && - text1.charAt(text1Length - x2 - 1) === - text2.charAt(text2Length - y2 - 1)) { - x2++; - y2++; - } - v2[k2Offset] = x2; - if (x2 > text1Length) { - // Ran off the left of the graph. - k2end += 2; - } else if (y2 > text2Length) { - // Ran off the top of the graph. - k2start += 2; - } else if (!front) { - k1Offset = vOffset + delta - k2; - if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { - x1 = v1[k1Offset]; - y1 = vOffset + x1 - k1Offset; - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - x2; - if (x1 >= x2) { - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - } - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - return [ - [ DIFF_DELETE, text1 ], - [ DIFF_INSERT, text2 ] - ]; - }; - - /** - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} x Index of split point in text1. - * @param {number} y Index of split point in text2. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { - var text1a, text1b, text2a, text2b, diffs, diffsb; - text1a = text1.substring(0, x); - text2a = text2.substring(0, y); - text1b = text1.substring(x); - text2b = text2.substring(y); - - // Compute both diffs serially. - diffs = this.DiffMain(text1a, text2a, false, deadline); - diffsb = this.DiffMain(text1b, text2b, false, deadline); - - return diffs.concat(diffsb); - }; - - /** - * Reduce the number of edits by eliminating semantically trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupSemantic = function(diffs) { - var changes, equalities, equalitiesLength, lastequality, - pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, - lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - // Number of characters that changed prior to the equality. - lengthInsertions1 = 0; - lengthDeletions1 = 0; - // Number of characters that changed after the equality. - lengthInsertions2 = 0; - lengthDeletions2 = 0; - while (pointer < diffs.length) { - if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found. - equalities[equalitiesLength++] = pointer; - lengthInsertions1 = lengthInsertions2; - lengthDeletions1 = lengthDeletions2; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = diffs[pointer][1]; - } else { // An insertion or deletion. - if (diffs[pointer][0] === DIFF_INSERT) { - lengthInsertions2 += diffs[pointer][1].length; - } else { - lengthDeletions2 += diffs[pointer][1].length; - } - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if (lastequality && (lastequality.length <= - Math.max(lengthInsertions1, lengthDeletions1)) && - (lastequality.length <= Math.max(lengthInsertions2, - lengthDeletions2))) { - // Duplicate record. - diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] ); - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - // Throw away the equality we just deleted. - equalitiesLength--; - // Throw away the previous equality (it needs to be reevaluated). - equalitiesLength--; - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - lengthInsertions1 = 0; // Reset the counters. - lengthDeletions1 = 0; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = null; - changes = true; - } - } - pointer++; - } - - // Normalize the diff. - if (changes) { - this.diffCleanupMerge(diffs); - } - - // Find any overlaps between deletions and insertions. - // e.g: abcxxxxxxdef - // -> abcxxxdef - // e.g: xxxabcdefxxx - // -> defxxxabc - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = 1; - while (pointer < diffs.length) { - if (diffs[pointer - 1][0] === DIFF_DELETE && - diffs[pointer][0] === DIFF_INSERT) { - deletion = diffs[pointer - 1][1]; - insertion = diffs[pointer][1]; - overlapLength1 = this.diffCommonOverlap(deletion, insertion); - overlapLength2 = this.diffCommonOverlap(insertion, deletion); - if (overlapLength1 >= overlapLength2) { - if (overlapLength1 >= deletion.length / 2 || - overlapLength1 >= insertion.length / 2) { - // Overlap found. Insert an equality and trim the surrounding edits. - diffs.splice( pointer, 0, [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] ); - diffs[pointer - 1][1] = - deletion.substring(0, deletion.length - overlapLength1); - diffs[pointer + 1][1] = insertion.substring(overlapLength1); - pointer++; - } - } else { - if (overlapLength2 >= deletion.length / 2 || - overlapLength2 >= insertion.length / 2) { - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - diffs.splice( pointer, 0, [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] ); - diffs[pointer - 1][0] = DIFF_INSERT; - diffs[pointer - 1][1] = - insertion.substring(0, insertion.length - overlapLength2); - diffs[pointer + 1][0] = DIFF_DELETE; - diffs[pointer + 1][1] = - deletion.substring(overlapLength2); - pointer++; - } - } - pointer++; - } - pointer++; - } - }; - - /** - * Determine if the suffix of one string is the prefix of another. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of the first - * string and the start of the second string. - * @private - */ - DiffMatchPatch.prototype.diffCommonOverlap = function(text1, text2) { - var text1Length, text2Length, textLength, - best, length, pattern, found; - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - // Eliminate the null case. - if (text1Length === 0 || text2Length === 0) { - return 0; - } - // Truncate the longer string. - if (text1Length > text2Length) { - text1 = text1.substring(text1Length - text2Length); - } else if (text1Length < text2Length) { - text2 = text2.substring(0, text1Length); - } - textLength = Math.min(text1Length, text2Length); - // Quick check for the worst case. - if (text1 === text2) { - return textLength; - } - - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: http://neil.fraser.name/news/2010/11/04/ - best = 0; - length = 1; - while (true) { - pattern = text1.substring(textLength - length); - found = text2.indexOf(pattern); - if (found === -1) { - return best; - } - length += found; - if (found === 0 || text1.substring(textLength - length) === - text2.substring(0, length)) { - best = length; - length++; - } - } - }; - - /** - * Split two texts into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {{chars1: string, chars2: string, lineArray: !Array.}} - * An object containing the encoded text1, the encoded text2 and - * the array of unique strings. - * The zeroth element of the array of unique strings is intentionally blank. - * @private - */ - DiffMatchPatch.prototype.diffLinesToChars = function(text1, text2) { - var lineArray, lineHash, chars1, chars2; - lineArray = []; // e.g. lineArray[4] === 'Hello\n' - lineHash = {}; // e.g. lineHash['Hello\n'] === 4 - - // '\x00' is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray[0] = ""; - - /** - * Split a text into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * Modifies linearray and linehash through being a closure. - * @param {string} text String to encode. - * @return {string} Encoded string. - * @private - */ - function diffLinesToCharsMunge(text) { - var chars, lineStart, lineEnd, lineArrayLength, line; - chars = ""; - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - lineStart = 0; - lineEnd = -1; - // Keeping our own length variable is faster than looking it up. - lineArrayLength = lineArray.length; - while (lineEnd < text.length - 1) { - lineEnd = text.indexOf("\n", lineStart); - if (lineEnd === -1) { - lineEnd = text.length - 1; - } - line = text.substring(lineStart, lineEnd + 1); - lineStart = lineEnd + 1; - - if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : - (lineHash[line] !== undefined)) { - chars += String.fromCharCode( lineHash[ line ] ); - } else { - chars += String.fromCharCode(lineArrayLength); - lineHash[line] = lineArrayLength; - lineArray[lineArrayLength++] = line; - } - } - return chars; - } - - chars1 = diffLinesToCharsMunge(text1); - chars2 = diffLinesToCharsMunge(text2); - return { - chars1: chars1, - chars2: chars2, - lineArray: lineArray - }; - }; - - /** - * Rehydrate the text in a diff from a string of line hashes to real lines of - * text. - * @param {!Array.} diffs Array of diff tuples. - * @param {!Array.} lineArray Array of unique strings. - * @private - */ - DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { - var x, chars, text, y; - for ( x = 0; x < diffs.length; x++ ) { - chars = diffs[x][1]; - text = []; - for ( y = 0; y < chars.length; y++ ) { - text[y] = lineArray[chars.charCodeAt(y)]; - } - diffs[x][1] = text.join(""); - } - }; - - /** - * Reorder and merge like edit sections. Merge equalities. - * Any edit section can move as long as it doesn't cross an equality. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupMerge = function(diffs) { - var pointer, countDelete, countInsert, textInsert, textDelete, - commonlength, changes; - diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - commonlength; - while (pointer < diffs.length) { - switch ( diffs[ pointer ][ 0 ] ) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - pointer++; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - pointer++; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (countDelete + countInsert > 1) { - if (countDelete !== 0 && countInsert !== 0) { - // Factor out any common prefixies. - commonlength = this.diffCommonPrefix(textInsert, textDelete); - if (commonlength !== 0) { - if ((pointer - countDelete - countInsert) > 0 && - diffs[pointer - countDelete - countInsert - 1][0] === - DIFF_EQUAL) { - diffs[pointer - countDelete - countInsert - 1][1] += - textInsert.substring(0, commonlength); - } else { - diffs.splice( 0, 0, [ DIFF_EQUAL, - textInsert.substring( 0, commonlength ) - ] ); - pointer++; - } - textInsert = textInsert.substring(commonlength); - textDelete = textDelete.substring(commonlength); - } - // Factor out any common suffixies. - commonlength = this.diffCommonSuffix(textInsert, textDelete); - if (commonlength !== 0) { - diffs[pointer][1] = textInsert.substring(textInsert.length - - commonlength) + diffs[pointer][1]; - textInsert = textInsert.substring(0, textInsert.length - - commonlength); - textDelete = textDelete.substring(0, textDelete.length - - commonlength); - } - } - // Delete the offending records and add the merged ones. - if (countDelete === 0) { - diffs.splice( pointer - countInsert, - countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); - } else if (countInsert === 0) { - diffs.splice( pointer - countDelete, - countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); - } else { - diffs.splice( pointer - countDelete - countInsert, - countDelete + countInsert, [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] ); - } - pointer = pointer - countDelete - countInsert + - (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; - } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { - // Merge this equality with the previous one. - diffs[pointer - 1][1] += diffs[pointer][1]; - diffs.splice(pointer, 1); - } else { - pointer++; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - } - if (diffs[diffs.length - 1][1] === "") { - diffs.pop(); // Remove the dummy entry at the end. - } - - // Second pass: look for single edits surrounded on both sides by equalities - // which can be shifted sideways to eliminate an equality. - // e.g: ABAC -> ABAC - changes = false; - pointer = 1; - // Intentionally ignore the first and last element (don't need checking). - while (pointer < diffs.length - 1) { - if (diffs[pointer - 1][0] === DIFF_EQUAL && - diffs[pointer + 1][0] === DIFF_EQUAL) { - // This is a single edit surrounded by equalities. - if ( diffs[ pointer ][ 1 ].substring( diffs[ pointer ][ 1 ].length - - diffs[ pointer - 1 ][ 1 ].length ) === diffs[ pointer - 1 ][ 1 ] ) { - // Shift the edit over the previous equality. - diffs[pointer][1] = diffs[pointer - 1][1] + - diffs[pointer][1].substring(0, diffs[pointer][1].length - - diffs[pointer - 1][1].length); - diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; - diffs.splice(pointer - 1, 1); - changes = true; - } else if ( diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === - diffs[ pointer + 1 ][ 1 ] ) { - // Shift the edit over the next equality. - diffs[pointer - 1][1] += diffs[pointer + 1][1]; - diffs[pointer][1] = - diffs[pointer][1].substring(diffs[pointer + 1][1].length) + - diffs[pointer + 1][1]; - diffs.splice(pointer + 1, 1); - changes = true; - } - } - pointer++; - } - // If shifts were made, the diff needs reordering and another shift sweep. - if (changes) { - this.diffCleanupMerge(diffs); - } - }; - - return function(o, n) { - var diff, output, text; - diff = new DiffMatchPatch(); - output = diff.DiffMain(o, n); - //console.log(output); - diff.diffCleanupEfficiency(output); - text = diff.diffPrettyHtml(output); - - return text; - }; -}()); -// jscs:enable - -(function() { - -// Deprecated QUnit.init - Ref #530 -// Re-initialize the configuration options -QUnit.init = function() { - var tests, banner, result, qunit, - config = QUnit.config; - - config.stats = { all: 0, bad: 0 }; - config.moduleStats = { all: 0, bad: 0 }; - config.started = 0; - config.updateRate = 1000; - config.blocking = false; - config.autostart = true; - config.autorun = false; - config.filter = ""; - config.queue = []; - - // Return on non-browser environments - // This is necessary to not break on node tests - if ( typeof window === "undefined" ) { - return; - } - - qunit = id( "qunit" ); - if ( qunit ) { - qunit.innerHTML = - "

      " + escapeText( document.title ) + "

      " + - "

      " + - "
      " + - "

      " + - "
        "; - } - - tests = id( "qunit-tests" ); - banner = id( "qunit-banner" ); - result = id( "qunit-testresult" ); - - if ( tests ) { - tests.innerHTML = ""; - } - - if ( banner ) { - banner.className = ""; - } - - if ( result ) { - result.parentNode.removeChild( result ); - } - - if ( tests ) { - result = document.createElement( "p" ); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests ); - result.innerHTML = "Running...
         "; - } -}; - -// Don't load the HTML Reporter on non-Browser environments -if ( typeof window === "undefined" ) { - return; -} - -var config = QUnit.config, - hasOwn = Object.prototype.hasOwnProperty, - defined = { - document: window.document !== undefined, - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch ( e ) { - return false; - } - }()) - }, - modulesList = []; - -/** -* Escape text for attribute or text content. -*/ -function escapeText( s ) { - if ( !s ) { - return ""; - } - s = s + ""; - - // Both single quotes and double quotes (for attributes) - return s.replace( /['"<>&]/g, function( s ) { - switch ( s ) { - case "'": - return "'"; - case "\"": - return """; - case "<": - return "<"; - case ">": - return ">"; - case "&": - return "&"; - } - }); -} - -/** - * @param {HTMLElement} elem - * @param {string} type - * @param {Function} fn - */ -function addEvent( elem, type, fn ) { - if ( elem.addEventListener ) { - - // Standards-based browsers - elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - - // support: IE <9 - elem.attachEvent( "on" + type, function() { - var event = window.event; - if ( !event.target ) { - event.target = event.srcElement || document; - } - - fn.call( elem, event ); - }); - } -} - -/** - * @param {Array|NodeList} elems - * @param {string} type - * @param {Function} fn - */ -function addEvents( elems, type, fn ) { - var i = elems.length; - while ( i-- ) { - addEvent( elems[ i ], type, fn ); - } -} - -function hasClass( elem, name ) { - return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; -} - -function addClass( elem, name ) { - if ( !hasClass( elem, name ) ) { - elem.className += ( elem.className ? " " : "" ) + name; - } -} - -function toggleClass( elem, name ) { - if ( hasClass( elem, name ) ) { - removeClass( elem, name ); - } else { - addClass( elem, name ); - } -} - -function removeClass( elem, name ) { - var set = " " + elem.className + " "; - - // Class name may appear multiple times - while ( set.indexOf( " " + name + " " ) >= 0 ) { - set = set.replace( " " + name + " ", " " ); - } - - // trim for prettiness - elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); -} - -function id( name ) { - return defined.document && document.getElementById && document.getElementById( name ); -} - -function getUrlConfigHtml() { - var i, j, val, - escaped, escapedTooltip, - selection = false, - len = config.urlConfig.length, - urlConfigHtml = ""; - - for ( i = 0; i < len; i++ ) { - val = config.urlConfig[ i ]; - if ( typeof val === "string" ) { - val = { - id: val, - label: val - }; - } - - escaped = escapeText( val.id ); - escapedTooltip = escapeText( val.tooltip ); - - if ( config[ val.id ] === undefined ) { - config[ val.id ] = QUnit.urlParams[ val.id ]; - } - - if ( !val.value || typeof val.value === "string" ) { - urlConfigHtml += ""; - } else { - urlConfigHtml += ""; - } - } - - return urlConfigHtml; -} - -// Handle "click" events on toolbar checkboxes and "change" for select menus. -// Updates the URL with the new state of `config.urlConfig` values. -function toolbarChanged() { - var updatedUrl, value, - field = this, - params = {}; - - // Detect if field is a select menu or a checkbox - if ( "selectedIndex" in field ) { - value = field.options[ field.selectedIndex ].value || undefined; - } else { - value = field.checked ? ( field.defaultValue || true ) : undefined; - } - - params[ field.name ] = value; - updatedUrl = setUrl( params ); - - if ( "hidepassed" === field.name && "replaceState" in window.history ) { - config[ field.name ] = value || false; - if ( value ) { - addClass( id( "qunit-tests" ), "hidepass" ); - } else { - removeClass( id( "qunit-tests" ), "hidepass" ); - } - - // It is not necessary to refresh the whole page - window.history.replaceState( null, "", updatedUrl ); - } else { - window.location = updatedUrl; - } -} - -function setUrl( params ) { - var key, - querystring = "?"; - - params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); - - for ( key in params ) { - if ( hasOwn.call( params, key ) ) { - if ( params[ key ] === undefined ) { - continue; - } - querystring += encodeURIComponent( key ); - if ( params[ key ] !== true ) { - querystring += "=" + encodeURIComponent( params[ key ] ); - } - querystring += "&"; - } - } - return location.protocol + "//" + location.host + - location.pathname + querystring.slice( 0, -1 ); -} - -function applyUrlParams() { - var selectedModule, - modulesList = id( "qunit-modulefilter" ), - filter = id( "qunit-filter-input" ).value; - - selectedModule = modulesList ? - decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) : - undefined; - - window.location = setUrl({ - module: ( selectedModule === "" ) ? undefined : selectedModule, - filter: ( filter === "" ) ? undefined : filter, - - // Remove testId filter - testId: undefined - }); -} - -function toolbarUrlConfigContainer() { - var urlConfigContainer = document.createElement( "span" ); - - urlConfigContainer.innerHTML = getUrlConfigHtml(); - addClass( urlConfigContainer, "qunit-url-config" ); - - // For oldIE support: - // * Add handlers to the individual elements instead of the container - // * Use "click" instead of "change" for checkboxes - addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged ); - addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); - - return urlConfigContainer; -} - -function toolbarLooseFilter() { - var filter = document.createElement( "form" ), - label = document.createElement( "label" ), - input = document.createElement( "input" ), - button = document.createElement( "button" ); - - addClass( filter, "qunit-filter" ); - - label.innerHTML = "Filter: "; - - input.type = "text"; - input.value = config.filter || ""; - input.name = "filter"; - input.id = "qunit-filter-input"; - - button.innerHTML = "Go"; - - label.appendChild( input ); - - filter.appendChild( label ); - filter.appendChild( button ); - addEvent( filter, "submit", function( ev ) { - applyUrlParams(); - - if ( ev && ev.preventDefault ) { - ev.preventDefault(); - } - - return false; - }); - - return filter; -} - -function toolbarModuleFilterHtml() { - var i, - moduleFilterHtml = ""; - - if ( !modulesList.length ) { - return false; - } - - modulesList.sort(function( a, b ) { - return a.localeCompare( b ); - }); - - moduleFilterHtml += "" + - ""; - - return moduleFilterHtml; -} - -function toolbarModuleFilter() { - var toolbar = id( "qunit-testrunner-toolbar" ), - moduleFilter = document.createElement( "span" ), - moduleFilterHtml = toolbarModuleFilterHtml(); - - if ( !toolbar || !moduleFilterHtml ) { - return false; - } - - moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); - moduleFilter.innerHTML = moduleFilterHtml; - - addEvent( moduleFilter.lastChild, "change", applyUrlParams ); - - toolbar.appendChild( moduleFilter ); -} - -function appendToolbar() { - var toolbar = id( "qunit-testrunner-toolbar" ); - - if ( toolbar ) { - toolbar.appendChild( toolbarUrlConfigContainer() ); - toolbar.appendChild( toolbarLooseFilter() ); - } -} - -function appendHeader() { - var header = id( "qunit-header" ); - - if ( header ) { - header.innerHTML = "" + header.innerHTML + " "; - } -} - -function appendBanner() { - var banner = id( "qunit-banner" ); - - if ( banner ) { - banner.className = ""; - } -} - -function appendTestResults() { - var tests = id( "qunit-tests" ), - result = id( "qunit-testresult" ); - - if ( result ) { - result.parentNode.removeChild( result ); - } - - if ( tests ) { - tests.innerHTML = ""; - result = document.createElement( "p" ); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests ); - result.innerHTML = "Running...
         "; - } -} - -function storeFixture() { - var fixture = id( "qunit-fixture" ); - if ( fixture ) { - config.fixture = fixture.innerHTML; - } -} - -function appendUserAgent() { - var userAgent = id( "qunit-userAgent" ); - - if ( userAgent ) { - userAgent.innerHTML = ""; - userAgent.appendChild( - document.createTextNode( - "QUnit " + QUnit.version + "; " + navigator.userAgent - ) - ); - } -} - -function appendTestsList( modules ) { - var i, l, x, z, test, moduleObj; - - for ( i = 0, l = modules.length; i < l; i++ ) { - moduleObj = modules[ i ]; - - if ( moduleObj.name ) { - modulesList.push( moduleObj.name ); - } - - for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { - test = moduleObj.tests[ x ]; - - appendTest( test.name, test.testId, moduleObj.name ); - } - } -} - -function appendTest( name, testId, moduleName ) { - var title, rerunTrigger, testBlock, assertList, - tests = id( "qunit-tests" ); - - if ( !tests ) { - return; - } - - title = document.createElement( "strong" ); - title.innerHTML = getNameHtml( name, moduleName ); - - rerunTrigger = document.createElement( "a" ); - rerunTrigger.innerHTML = "Rerun"; - rerunTrigger.href = setUrl({ testId: testId }); - - testBlock = document.createElement( "li" ); - testBlock.appendChild( title ); - testBlock.appendChild( rerunTrigger ); - testBlock.id = "qunit-test-output-" + testId; - - assertList = document.createElement( "ol" ); - assertList.className = "qunit-assert-list"; - - testBlock.appendChild( assertList ); - - tests.appendChild( testBlock ); -} - -// HTML Reporter initialization and load -QUnit.begin(function( details ) { - var qunit = id( "qunit" ); - - // Fixture is the only one necessary to run without the #qunit element - storeFixture(); - - if ( qunit ) { - qunit.innerHTML = - "

        " + escapeText( document.title ) + "

        " + - "

        " + - "
        " + - "

        " + - "
          "; - } - - appendHeader(); - appendBanner(); - appendTestResults(); - appendUserAgent(); - appendToolbar(); - appendTestsList( details.modules ); - toolbarModuleFilter(); - - if ( qunit && config.hidepassed ) { - addClass( qunit.lastChild, "hidepass" ); - } -}); - -QUnit.done(function( details ) { - var i, key, - banner = id( "qunit-banner" ), - tests = id( "qunit-tests" ), - html = [ - "Tests completed in ", - details.runtime, - " milliseconds.
          ", - "", - details.passed, - " assertions of ", - details.total, - " passed, ", - details.failed, - " failed." - ].join( "" ); - - if ( banner ) { - banner.className = details.failed ? "qunit-fail" : "qunit-pass"; - } - - if ( tests ) { - id( "qunit-testresult" ).innerHTML = html; - } - - if ( config.altertitle && defined.document && document.title ) { - - // show ✖ for good, ✔ for bad suite result in title - // use escape sequences in case file gets loaded with non-utf-8-charset - document.title = [ - ( details.failed ? "\u2716" : "\u2714" ), - document.title.replace( /^[\u2714\u2716] /i, "" ) - ].join( " " ); - } - - // clear own sessionStorage items if all tests passed - if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { - for ( i = 0; i < sessionStorage.length; i++ ) { - key = sessionStorage.key( i++ ); - if ( key.indexOf( "qunit-test-" ) === 0 ) { - sessionStorage.removeItem( key ); - } - } - } - - // scroll back to top to show results - if ( config.scrolltop && window.scrollTo ) { - window.scrollTo( 0, 0 ); - } -}); - -function getNameHtml( name, module ) { - var nameHtml = ""; - - if ( module ) { - nameHtml = "" + escapeText( module ) + ": "; - } - - nameHtml += "" + escapeText( name ) + ""; - - return nameHtml; -} - -QUnit.testStart(function( details ) { - var running, testBlock, bad; - - testBlock = id( "qunit-test-output-" + details.testId ); - if ( testBlock ) { - testBlock.className = "running"; - } else { - - // Report later registered tests - appendTest( details.name, details.testId, details.module ); - } - - running = id( "qunit-testresult" ); - if ( running ) { - bad = QUnit.config.reorder && defined.sessionStorage && - +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name ); - - running.innerHTML = ( bad ? - "Rerunning previously failed test:
          " : - "Running:
          " ) + - getNameHtml( details.name, details.module ); - } - -}); - -QUnit.log(function( details ) { - var assertList, assertLi, - message, expected, actual, - testItem = id( "qunit-test-output-" + details.testId ); - - if ( !testItem ) { - return; - } - - message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); - message = "" + message + ""; - message += "@ " + details.runtime + " ms"; - - // pushFailure doesn't provide details.expected - // when it calls, it's implicit to also not show expected and diff stuff - // Also, we need to check details.expected existence, as it can exist and be undefined - if ( !details.result && hasOwn.call( details, "expected" ) ) { - expected = escapeText( QUnit.dump.parse( details.expected ) ); - actual = escapeText( QUnit.dump.parse( details.actual ) ); - message += ""; - - if ( actual !== expected ) { - message += "" + - ""; - } else { - if ( expected.indexOf( "[object Array]" ) !== -1 || - expected.indexOf( "[object Object]" ) !== -1 ) { - message += ""; - } - } - - if ( details.source ) { - message += ""; - } - - message += "
          Expected:
          " +
          -			expected +
          -			"
          Result:
          " +
          -				actual + "
          Diff:
          " +
          -				QUnit.diff( expected, actual ) + "
          Message: " + - "Diff suppressed as the depth of object is more than current max depth (" + - QUnit.config.maxDepth + ").

          Hint: Use QUnit.dump.maxDepth to " + - " run with a higher max depth or " + - "Rerun without max depth.

          Source:
          " +
          -				escapeText( details.source ) + "
          "; - - // this occours when pushFailure is set and we have an extracted stack trace - } else if ( !details.result && details.source ) { - message += "" + - "" + - "
          Source:
          " +
          -			escapeText( details.source ) + "
          "; - } - - assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; - - assertLi = document.createElement( "li" ); - assertLi.className = details.result ? "pass" : "fail"; - assertLi.innerHTML = message; - assertList.appendChild( assertLi ); -}); - -QUnit.testDone(function( details ) { - var testTitle, time, testItem, assertList, - good, bad, testCounts, skipped, - tests = id( "qunit-tests" ); - - if ( !tests ) { - return; - } - - testItem = id( "qunit-test-output-" + details.testId ); - - assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; - - good = details.passed; - bad = details.failed; - - // store result when possible - if ( config.reorder && defined.sessionStorage ) { - if ( bad ) { - sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); - } else { - sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); - } - } - - if ( bad === 0 ) { - addClass( assertList, "qunit-collapsed" ); - } - - // testItem.firstChild is the test name - testTitle = testItem.firstChild; - - testCounts = bad ? - "" + bad + ", " + "" + good + ", " : - ""; - - testTitle.innerHTML += " (" + testCounts + - details.assertions.length + ")"; - - if ( details.skipped ) { - testItem.className = "skipped"; - skipped = document.createElement( "em" ); - skipped.className = "qunit-skipped-label"; - skipped.innerHTML = "skipped"; - testItem.insertBefore( skipped, testTitle ); - } else { - addEvent( testTitle, "click", function() { - toggleClass( assertList, "qunit-collapsed" ); - }); - - testItem.className = bad ? "fail" : "pass"; - - time = document.createElement( "span" ); - time.className = "runtime"; - time.innerHTML = details.runtime + " ms"; - testItem.insertBefore( time, assertList ); - } -}); - -if ( defined.document ) { - if ( document.readyState === "complete" ) { - QUnit.load(); - } else { - addEvent( window, "load", QUnit.load ); - } -} else { - config.pageLoaded = true; - config.autorun = true; -} - -})(); \ No newline at end of file From 33be27a53d85f60bf41d0efa5231bbcbd5b90872 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 22:15:29 -0300 Subject: [PATCH 31/63] The default path attribute should be to the whole site --- src/main/java/org/jscookie/Cookies.java | 2 +- src/test/java/org/jscookie/test/unit/CookiesWriteTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 29a39db..e6e8a8e 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -23,7 +23,7 @@ public final class Cookies implements CookiesDefinition { private HttpServletRequest request; private HttpServletResponse response; - private CookiesDefinition.Attributes defaults = Attributes.empty(); + private CookiesDefinition.Attributes defaults = Attributes.empty().path( "/" ); private CookiesDefinition.Converter converter; private ObjectMapper mapper = new ObjectMapper(); diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index 8b65398..375fa87 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -34,7 +34,7 @@ public void simple_write() throws UnsupportedEncodingException { Cookie actual = argument.getValue(); Assert.assertEquals( "c", actual.getName() ); Assert.assertEquals( "v", actual.getValue() ); - Assert.assertEquals( null, actual.getPath() ); + Assert.assertEquals( "/", actual.getPath() ); Assert.assertEquals( null, actual.getDomain() ); Assert.assertEquals( false, actual.getSecure() ); Assert.assertEquals( -1, actual.getMaxAge() ); From 9d94caf607028ced0b389a00dbe1f85a65fc0e62 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 22:21:29 -0300 Subject: [PATCH 32/63] Do not spam the tests results when debugging --- .../jscookie/test/integration/qunit/QUnitPageObject.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java b/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java index 3ecb552..10b9c32 100644 --- a/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java +++ b/src/test/java/org/jscookie/test/integration/qunit/QUnitPageObject.java @@ -28,11 +28,11 @@ public QUnitResults apply( WebDriver input ) { String result = ( String )js.executeScript( "return window.global_test_results && JSON.stringify(window.global_test_results)" ); - System.out.println( "Waiting for 'window.global_test_results': " + result ); - if ( result == null ) { + if ( debug.is( true ) ) { return null; } - if ( debug.is( true ) ) { + System.out.println( "Waiting for 'window.global_test_results': " + result ); + if ( result == null ) { return null; } try { From df4978cb4893cfcb20e8629ad1eca22f5761aba3 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 22:22:05 -0300 Subject: [PATCH 33/63] Add remaining classes to arquillian --- .../integration/encoding/CookiesEncodingIT.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java index e14bb53..564aab9 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java +++ b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java @@ -1,5 +1,6 @@ package org.jscookie.test.integration.encoding; +import java.io.File; import java.net.URL; import java.util.concurrent.TimeUnit; @@ -13,6 +14,7 @@ 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; @@ -28,7 +30,7 @@ @RunWith( Arquillian.class ) public class CookiesEncodingIT { - private static Debug debug = Debug.FALSE; + private static Debug debug = Debug.TRUE; @Deployment public static Archive createDeployment() { @@ -40,7 +42,19 @@ public static Archive createDeployment() { .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", + "org.eclipse.jdt:org.eclipse.jdt.annotation", + "com.fasterxml.jackson.core:jackson-databind" + ) + .withTransitivity() + .as( File.class ) + ) .merge( qunitFiles, "/", Filters.includeAll() ); System.out.println( " ----- LOGGING THE FILES ADDED TO JBOSS" ); From b50b89a6ec4452d597c8b32a2aacd1a22bc57913 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 22:22:38 -0300 Subject: [PATCH 34/63] Complement last commit with the dependencies --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index d1ac13f..81311bd 100644 --- a/pom.xml +++ b/pom.xml @@ -181,6 +181,13 @@ 7.2.0.Final test
          + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-bom + 2.2.0-beta-2 + pom + import + org.mockito mockito-all From 1bfb21192587344676bde09614ac027c4e812a4d Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 22:22:51 -0300 Subject: [PATCH 35/63] Provide a proper return for each request --- .../integration/encoding/EncodingServlet.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java index c5867e6..3ef3eee 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java @@ -8,12 +8,46 @@ 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 { - System.out.println( "EXECUTED SERVLET: " + request.getParameter( "name" ) ); + String name = request.getParameter( "name" ); + + System.out.println( "--------------------" ); + System.out.println( "Testing: " + name ); + + Cookies cookies = new Cookies( request, response ); + String value = cookies.get( name ); + + System.out.println( "Value: " + value ); + System.out.println( "--------------------" ); + + cookies.set( name, value ); + + response.setContentType( "application/json" ); + new ObjectMapper() + .writeValue( response.getOutputStream(), new Result( name, value ) ); } } + +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 From 35a6f8394d1b7a6e826fbaccb63fa95a1050c732 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 24 May 2015 22:38:30 -0300 Subject: [PATCH 36/63] Make it clear when a cookie is not found --- .../jscookie/test/integration/encoding/EncodingServlet.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java index 3ef3eee..326566d 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java @@ -26,6 +26,10 @@ public void doGet( HttpServletRequest request, HttpServletResponse response ) Cookies cookies = new Cookies( request, response ); String value = cookies.get( name ); + if ( value == null ) { + throw new NullPointerException( "Cookie not found with name: " + name ); + } + System.out.println( "Value: " + value ); System.out.println( "--------------------" ); From 36073af913ad7b353a407652de9a985b8835b104 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sat, 30 May 2015 10:20:05 -0300 Subject: [PATCH 37/63] Encode/decode without using URLEncoder/URLDecoder --- src/main/java/org/jscookie/Cookies.java | 97 ++++++++----------- .../java/org/jscookie/CookiesDefinition.java | 16 +-- .../test/unit/CookiesDecodingTest.java | 32 ++++++ .../test/unit/CookiesEncodingTest.java | 35 +++++++ .../jscookie/test/unit/CookiesWriteTest.java | 10 +- 5 files changed, 117 insertions(+), 73 deletions(-) create mode 100644 src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java create mode 100644 src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index e6e8a8e..c422273 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -1,14 +1,12 @@ package org.jscookie; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ResourceBundle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -52,19 +50,13 @@ public synchronized String get( String name ) { } for ( Cookie cookie : cookies ) { - String decodedName = decodeName( cookie ); + String decodedName = decode( cookie.getName() ); if ( !name.equals( decodedName ) ) { continue; } - - String decodedValue = decodeValue( cookie, decodedName ); - - if ( decodedValue == null ) { - continue; - } - - return decodedValue; + + return decodeValue( cookie, decodedName ); } return null; @@ -100,13 +92,8 @@ public Map get() { } for ( Cookie cookie : cookies ) { - String decodedName = decodeName( cookie ); + String decodedName = decode( cookie.getName() ); String decodedValue = decodeValue( cookie, decodedName ); - - if ( decodedValue == null ) { - continue; - } - result.put( decodedName, decodedValue ); } @@ -114,7 +101,7 @@ public Map get() { } @Override - public synchronized void set( String name, String value, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { + public synchronized void set( String name, String value, CookiesDefinition.Attributes attributes ) { if ( name == null || name.length() == 0 ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } @@ -125,8 +112,8 @@ public synchronized void set( String name, String value, CookiesDefinition.Attri throw new IllegalArgumentException(); } - String encodedName = URLEncoder.encode( name, StandardCharsets.UTF_8.name() ); - String encodedValue = URLEncoder.encode( value, StandardCharsets.UTF_8.name() ); + String encodedName = encode( name ); + String encodedValue = encode( value ); Cookie cookie = new Cookie( encodedName, encodedValue ); attributes = extend( defaults, attributes ); @@ -156,27 +143,19 @@ public synchronized void set( String name, String value, CookiesDefinition.Attri @Override public void set( String name, int value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { - try { - set( name, String.valueOf( value ), attributes ); - } catch ( UnsupportedEncodingException e ) { - throw new CookieSerializationException( e ); - } + set( name, String.valueOf( value ), attributes ); } @Override public void set( String name, boolean value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { - try { - set( name, String.valueOf( value ), attributes ); - } catch ( UnsupportedEncodingException e ) { - throw new CookieSerializationException( e ); - } + set( name, String.valueOf( value ), attributes ); } @Override public void set( String name, List value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { try { set( name, mapper.writeValueAsString( value ), attributes ); - } catch ( UnsupportedEncodingException | JsonProcessingException e ) { + } catch ( JsonProcessingException e ) { throw new CookieSerializationException( e ); } } @@ -185,13 +164,13 @@ public void set( String name, List value, CookiesDefinition.Attributes at public void set( String name, CookieValue value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { try { set( name, mapper.writeValueAsString( value ), attributes ); - } catch ( UnsupportedEncodingException | JsonProcessingException e ) { + } catch ( JsonProcessingException e ) { throw new CookieSerializationException( e ); } } @Override - public void set( String name, String value ) throws UnsupportedEncodingException { + public void set( String name, String value ) { if ( name == null || name.length() == 0 ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } @@ -207,12 +186,8 @@ public void set( String name, int value ) throws CookieSerializationException { } @Override - public void set( String name, boolean value ) throws CookieSerializationException { - try { - set( name, String.valueOf( value ) ); - } catch ( UnsupportedEncodingException e ) { - throw new CookieSerializationException( e ); - } + public void set( String name, boolean value ) { + set( name, String.valueOf( value ) ); } @Override @@ -226,7 +201,7 @@ public void set( String name, CookieValue value ) throws CookieSerializationExce } @Override - public void remove( String name, CookiesDefinition.Attributes attributes ) throws UnsupportedEncodingException { + public void remove( String name, CookiesDefinition.Attributes attributes ) { if ( name == null || name.length() == 0 ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } @@ -240,7 +215,7 @@ public void remove( String name, CookiesDefinition.Attributes attributes ) throw } @Override - public void remove( String name ) throws UnsupportedEncodingException { + public void remove( String name ) { if ( name == null || name.length() == 0 ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } @@ -264,16 +239,32 @@ private Attributes extend( CookiesDefinition.Attributes a, CookiesDefinition.Att return Attributes.empty().merge( a ).merge( b ); } - private String decodeName( Cookie cookie ) { - try { - return URLDecoder.decode( cookie.getName(), StandardCharsets.UTF_8.name() ); - } catch ( UnsupportedEncodingException e ) { - e.printStackTrace(); + private String encode( String plain ) { + String encoded = plain; + for ( int i = 0; i < plain.length(); i++ ) { + Character character = plain.charAt( i ); + String hex = "%" + Integer.toHexString( character ).toUpperCase(); + encoded = encoded.replace( character.toString(), hex ); } - return null; + return encoded; } - private @Nullable String decodeValue( Cookie cookie, String decodedName ) { + private String decode( String encoded ) { + Pattern pattern = Pattern.compile( "(%[0-9A-Z]{2})+" ); + Matcher matcher = pattern.matcher( encoded ); + if ( !matcher.matches() ) { + return encoded; + } + for ( int groupIndex = 0; groupIndex < matcher.groupCount(); groupIndex++ ) { + String character = matcher.group( groupIndex ); + String hexChar = character.substring( 1, character.length() ); + Character decoded = ( char )Integer.parseInt( hexChar, 16 ); + encoded = encoded.replace( character, decoded.toString() ); + } + return encoded; + } + + private String decodeValue( Cookie cookie, String decodedName ) { String decodedValue = null; if ( converter != null ) { @@ -285,11 +276,7 @@ private String decodeName( Cookie cookie ) { } if ( decodedValue == null ) { - try { - decodedValue = URLDecoder.decode( cookie.getValue(), StandardCharsets.UTF_8.name() ); - } catch ( UnsupportedEncodingException e ) { - e.printStackTrace(); - } + decodedValue = decode( cookie.getValue() ); } return decodedValue; diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index a6ebb82..1ddc661 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -1,6 +1,5 @@ package org.jscookie; -import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; @@ -49,13 +48,9 @@ interface CookiesDefinition { * 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. * - * @throws UnsupportedEncodingException - * If the current encoding being used to encode/decode the cookie name or value is not - * supported - * * @see #get(String) */ - void set( String name, String value, Attributes attributes ) throws UnsupportedEncodingException; + void set( String name, String value, Attributes attributes ); /** * Create or update an existing cookie extending the default attributes and serializing the typed value @@ -90,7 +85,7 @@ interface CookiesDefinition { * * @see #set(String, String, Attributes) */ - void set( String name, String value ) throws UnsupportedEncodingException; + void set( String name, String value ); /** * Create or update an existing cookie using the default attributes and serializing the typed value @@ -129,19 +124,16 @@ interface CookiesDefinition { * 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 * - * @throws UnsupportedEncodingException - * If the current encoding being used to decode the cookie name is not supported - * * @see #get(String) */ - void remove( String name, Attributes attributes ) throws UnsupportedEncodingException; + void remove( String name, Attributes attributes ); /** * Remove an existing cookie using the default attributes * * @see #remove(String, Attributes) */ - void remove( String name ) throws UnsupportedEncodingException; + void remove( String name ); /** * Change the default attributes of this instance 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..10ec27f --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java @@ -0,0 +1,32 @@ +package org.jscookie.test.unit; + +import javax.servlet.http.Cookie; + +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 = new Cookies( request, response ); + } + + @Test + public void character_not_allowed_in_name_and_value() { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie( "%3B", "%3B" ) + }); + String actual = cookies.get( ";" ); + 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..0d1e400 --- /dev/null +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -0,0 +1,35 @@ +package org.jscookie.test.unit; + +import javax.servlet.http.Cookie; + +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.ArgumentCaptor; +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 = new Cookies( request, response ); + } + + @Test + public void character_not_allowed_in_name_and_value() { + cookies.set( ";", ";" ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "%3B", actual.getName() ); + Assert.assertEquals( "%3B", actual.getValue() ); + } +} diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index 375fa87..47718cb 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -1,7 +1,5 @@ package org.jscookie.test.unit; -import java.io.UnsupportedEncodingException; - import javax.servlet.http.Cookie; import org.jscookie.Cookies; @@ -25,7 +23,7 @@ public void before() { } @Test - public void simple_write() throws UnsupportedEncodingException { + public void simple_write() { cookies.set( "c", "v" ); ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); @@ -41,7 +39,7 @@ public void simple_write() throws UnsupportedEncodingException { } @Test - public void simple_write_with_default_attributes() throws UnsupportedEncodingException { + public void simple_write_with_default_attributes() { cookies.setDefaults(Cookies.Attributes.empty() .path( "/" ) .domain( "site.com" ) @@ -63,7 +61,7 @@ public void simple_write_with_default_attributes() throws UnsupportedEncodingExc } @Test - public void simple_write_with_attributes() throws UnsupportedEncodingException { + public void simple_write_with_attributes() { cookies.set( "c", "v", Cookies.Attributes.empty() .path( "/" ) .domain( "example.com" ) @@ -84,7 +82,7 @@ public void simple_write_with_attributes() throws UnsupportedEncodingException { } @Test - public void simple_write_overriding_default_attributes() throws UnsupportedEncodingException { + public void simple_write_overriding_default_attributes() { cookies.setDefaults(Cookies.Attributes.empty() .path( "/path/" ) .secure( true ) From 88c540e5634e506332b207c6c645dba3acb2727b Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sat, 30 May 2015 15:01:15 -0300 Subject: [PATCH 38/63] Should not encode characters allowed in both name and value --- src/main/java/org/jscookie/Cookies.java | 23 ++++++++++++++++ .../test/unit/CookiesEncodingTest.java | 27 ++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index c422273..d7b72cc 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -243,6 +243,29 @@ private String encode( String plain ) { String encoded = plain; for ( int i = 0; i < plain.length(); i++ ) { Character character = plain.charAt( i ); + + boolean isDigit = Character.isDigit( character ); + if ( isDigit ) { + continue; + } + + boolean isAsciiUppercaseLetter = character >= 'A' && character <= 'Z'; + if ( isAsciiUppercaseLetter ) { + continue; + } + + boolean isAsciiLowercaseLetter = character >= 'a' && character <= 'z'; + if ( isAsciiLowercaseLetter ) { + continue; + } + + boolean isAllowed = character == '!' || character == '#' || character == '$' || + character == '&' || character == '\'' || character == '*' || character == '+' || + character == '-' || character == '.' || character == '^' || character == '_' || + character == '`' || character == '|' || character == '~'; + if ( isAllowed ) { + continue; + } String hex = "%" + Integer.toHexString( character ).toUpperCase(); encoded = encoded.replace( character.toString(), hex ); } diff --git a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java index 0d1e400..a6e44ca 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -23,13 +23,34 @@ public void before() { @Test public void character_not_allowed_in_name_and_value() { - cookies.set( ";", ";" ); + cookies.set( ";,\\\" ", ";,\\\" " ); ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); Mockito.verify( response ).addCookie( argument.capture() ); Cookie actual = argument.getValue(); - Assert.assertEquals( "%3B", actual.getName() ); - Assert.assertEquals( "%3B", actual.getValue() ); + Assert.assertEquals( "%3B%2C%5C%22%20", actual.getName() ); + Assert.assertEquals( "%3B%2C%5C%22%20", actual.getValue() ); + } + + @Test + public void characters_allowed_in_name_and_value() { + cookies.set( + "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~", + "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~" + ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( + "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~", + actual.getValue() + ); + Assert.assertEquals( + "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~", + actual.getName() + ); } } From c53640e36bd59d1810d9be89b5c927d99bdf0f68 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sat, 30 May 2015 15:46:06 -0300 Subject: [PATCH 39/63] Align the conditons --- src/main/java/org/jscookie/Cookies.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index d7b72cc..e005597 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -259,13 +259,16 @@ private String encode( String plain ) { continue; } - boolean isAllowed = character == '!' || character == '#' || character == '$' || - character == '&' || character == '\'' || character == '*' || character == '+' || - character == '-' || character == '.' || character == '^' || character == '_' || - character == '`' || character == '|' || character == '~'; + boolean isAllowed = + character == '!' || character == '#' || character == '$' || + character == '&' || character == '\'' || character == '*' || + character == '+' || character == '-' || character == '.' || + character == '^' || character == '_' || character == '`' || + character == '|' || character == '~'; if ( isAllowed ) { continue; } + String hex = "%" + Integer.toHexString( character ).toUpperCase(); encoded = encoded.replace( character.toString(), hex ); } From 2d6b56ed69ea3ad3656a1c78c62b9aa2449c48fa Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 31 May 2015 18:03:56 -0300 Subject: [PATCH 40/63] Correctly decode characters with more than 1 byte using UTF-8 --- src/main/java/org/jscookie/Cookies.java | 24 ++++++++++++++----- .../test/unit/CookiesDecodingTest.java | 12 ++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index e005597..099d9b1 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -1,6 +1,8 @@ package org.jscookie; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -276,18 +278,28 @@ private String encode( String plain ) { } private String decode( String encoded ) { + String decoded = encoded; Pattern pattern = Pattern.compile( "(%[0-9A-Z]{2})+" ); Matcher matcher = pattern.matcher( encoded ); if ( !matcher.matches() ) { - return encoded; + return decoded; } for ( int groupIndex = 0; groupIndex < matcher.groupCount(); groupIndex++ ) { - String character = matcher.group( groupIndex ); - String hexChar = character.substring( 1, character.length() ); - Character decoded = ( char )Integer.parseInt( hexChar, 16 ); - encoded = encoded.replace( character, decoded.toString() ); + String encodedChar = matcher.group( groupIndex ); + 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 encoded; + return decoded; } private String decodeValue( Cookie cookie, String decodedName ) { diff --git a/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java index 10ec27f..9abe1db 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java @@ -1,5 +1,7 @@ package org.jscookie.test.unit; +import java.io.UnsupportedEncodingException; + import javax.servlet.http.Cookie; import org.jscookie.Cookies; @@ -29,4 +31,14 @@ public void character_not_allowed_in_name_and_value() { String expected = ";"; Assert.assertEquals( expected, actual ); } + + @Test + public void character_with_3_bytes() throws UnsupportedEncodingException { + Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { + new Cookie("c", "%E4%BA%AC") + }); + String actual = cookies.get( "c" ); + String expected = "京"; + Assert.assertEquals( expected, actual ); + } } From 90bdec59629787d4918e5f5453bc6941e9432ab6 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 31 May 2015 20:49:19 -0300 Subject: [PATCH 41/63] Adjust encoding for characters greater than 1 byte --- src/main/java/org/jscookie/Cookies.java | 20 +++++++++++++++++-- .../test/unit/CookiesDecodingTest.java | 4 +--- .../test/unit/CookiesEncodingTest.java | 11 ++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 099d9b1..120646b 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -1,5 +1,6 @@ package org.jscookie; +import java.io.CharArrayWriter; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; @@ -271,8 +272,23 @@ private String encode( String plain ) { continue; } - String hex = "%" + Integer.toHexString( character ).toUpperCase(); - encoded = encoded.replace( character.toString(), hex ); + try { + CharArrayWriter hexSequence = new CharArrayWriter(); + byte[] bytes = character.toString().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; } diff --git a/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java index 9abe1db..8bb8205 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java @@ -1,7 +1,5 @@ package org.jscookie.test.unit; -import java.io.UnsupportedEncodingException; - import javax.servlet.http.Cookie; import org.jscookie.Cookies; @@ -33,7 +31,7 @@ public void character_not_allowed_in_name_and_value() { } @Test - public void character_with_3_bytes() throws UnsupportedEncodingException { + public void character_with_3_bytes() { Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { new Cookie("c", "%E4%BA%AC") }); diff --git a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java index a6e44ca..9aa44a7 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -53,4 +53,15 @@ public void characters_allowed_in_name_and_value() { actual.getName() ); } + + @Test + public void character_with_3_bytes() { + cookies.set( "c", "京" ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "%E4%BA%AC", actual.getValue() ); + } } From 94e6f8e4f979096152e5037e58520a6390b2dd7a Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 31 May 2015 22:56:45 -0300 Subject: [PATCH 42/63] There's no need to put the Attributes definition inside the interface --- .../org/jscookie/AttributesDefinition.java | 8 +++++ src/main/java/org/jscookie/Cookies.java | 22 ++++++------ .../java/org/jscookie/CookiesDefinition.java | 35 ++++++++----------- 3 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/jscookie/AttributesDefinition.java diff --git a/src/main/java/org/jscookie/AttributesDefinition.java b/src/main/java/org/jscookie/AttributesDefinition.java new file mode 100644 index 0000000..c631b5a --- /dev/null +++ b/src/main/java/org/jscookie/AttributesDefinition.java @@ -0,0 +1,8 @@ +package org.jscookie; + +public abstract class AttributesDefinition { + abstract Expiration expires(); + abstract String path(); + abstract String domain(); + abstract Boolean secure(); +} diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 120646b..1e06a66 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -24,7 +24,7 @@ public final class Cookies implements CookiesDefinition { private HttpServletRequest request; private HttpServletResponse response; - private CookiesDefinition.Attributes defaults = Attributes.empty().path( "/" ); + private AttributesDefinition defaults = Attributes.empty().path( "/" ); private CookiesDefinition.Converter converter; private ObjectMapper mapper = new ObjectMapper(); @@ -104,7 +104,7 @@ public Map get() { } @Override - public synchronized void set( String name, String value, CookiesDefinition.Attributes attributes ) { + 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" ) ); } @@ -145,17 +145,17 @@ public synchronized void set( String name, String value, CookiesDefinition.Attri } @Override - public void set( String name, int value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { + 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, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { + 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, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { + public void set( String name, List value, AttributesDefinition attributes ) throws CookieSerializationException { try { set( name, mapper.writeValueAsString( value ), attributes ); } catch ( JsonProcessingException e ) { @@ -164,7 +164,7 @@ public void set( String name, List value, CookiesDefinition.Attributes at } @Override - public void set( String name, CookieValue value, CookiesDefinition.Attributes attributes ) throws CookieSerializationException { + public void set( String name, CookieValue value, AttributesDefinition attributes ) throws CookieSerializationException { try { set( name, mapper.writeValueAsString( value ), attributes ); } catch ( JsonProcessingException e ) { @@ -204,7 +204,7 @@ public void set( String name, CookieValue value ) throws CookieSerializationExce } @Override - public void remove( String name, CookiesDefinition.Attributes attributes ) { + public void remove( String name, AttributesDefinition attributes ) { if ( name == null || name.length() == 0 ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } @@ -226,7 +226,7 @@ public void remove( String name ) { } @Override - public void setDefaults( CookiesDefinition.Attributes defaults ) { + public void setDefaults( AttributesDefinition defaults ) { if ( defaults == null ) { throw new IllegalArgumentException(); } @@ -238,7 +238,7 @@ public Cookies withConverter( CookiesDefinition.Converter converter ) { return new Cookies( request, response, converter ); } - private Attributes extend( CookiesDefinition.Attributes a, CookiesDefinition.Attributes b ) { + private Attributes extend( AttributesDefinition a, AttributesDefinition b ) { return Attributes.empty().merge( a ).merge( b ); } @@ -336,7 +336,7 @@ private String decodeValue( Cookie cookie, String decodedName ) { return decodedValue; } - public static class Attributes extends CookiesDefinition.Attributes { + public static class Attributes extends AttributesDefinition { private Expiration expires; private String path; private String domain; @@ -388,7 +388,7 @@ public Attributes secure( @Nullable Boolean secure ) { return this; } - private Attributes merge( CookiesDefinition.Attributes reference ) { + private Attributes merge( AttributesDefinition reference ) { if ( reference.path() != null ) { path = reference.path(); } diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index 1ddc661..91068d8 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -50,40 +50,40 @@ interface CookiesDefinition { * * @see #get(String) */ - void set( String name, String value, Attributes attributes ); + 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, Attributes) + * @see #set(String, String, AttributesDefinition) */ - void set( String name, int value, Attributes attributes ) throws CookieSerializationException; + 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, Attributes) + * @see #set(String, String, AttributesDefinition) */ - void set( String name, boolean value, Attributes attributes ) throws CookieSerializationException; + 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, Attributes) + * @see #set(String, String, AttributesDefinition) */ - void set( String name, List value, Attributes attributes ) throws CookieSerializationException; + 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, Attributes) + * @see #set(String, String, AttributesDefinition) */ - void set( String name, CookieValue value, Attributes attributes ) throws CookieSerializationException; + void set( String name, CookieValue value, AttributesDefinition attributes ) throws CookieSerializationException; /** * Create or update an existing cookie using the default attributes * - * @see #set(String, String, Attributes) + * @see #set(String, String, AttributesDefinition) */ void set( String name, String value ); @@ -126,36 +126,29 @@ interface CookiesDefinition { * * @see #get(String) */ - void remove( String name, Attributes attributes ); + void remove( String name, AttributesDefinition attributes ); /** * Remove an existing cookie using the default attributes * - * @see #remove(String, Attributes) + * @see #remove(String, AttributesDefinition) */ void remove( String name ); /** * Change the default attributes of this instance */ - void setDefaults( Attributes attributes ); + void setDefaults( AttributesDefinition attributes ); /** * 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, Attributes)} and {@link #get(String)}, will run the converter first + * {@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( Converter converter ); - abstract class Attributes { - abstract Expiration expires(); - abstract String path(); - abstract String domain(); - abstract Boolean secure(); - } - abstract class Converter { /** * Determine the decoding strategy of a cookie. The return will be used as the cookie value From f6c1595cd4cea92831dd6f4303a564114ac961f7 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 31 May 2015 23:06:02 -0300 Subject: [PATCH 43/63] Separate the strategy internal interface from the Cookies definition --- src/main/java/org/jscookie/ConverterStrategy.java | 10 ++++++++++ src/main/java/org/jscookie/Cookies.java | 8 ++++---- src/main/java/org/jscookie/CookiesDefinition.java | 11 +---------- 3 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/jscookie/ConverterStrategy.java diff --git a/src/main/java/org/jscookie/ConverterStrategy.java b/src/main/java/org/jscookie/ConverterStrategy.java new file mode 100644 index 0000000..542060e --- /dev/null +++ b/src/main/java/org/jscookie/ConverterStrategy.java @@ -0,0 +1,10 @@ +package org.jscookie; + +abstract class 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/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 1e06a66..877d0d8 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -25,13 +25,13 @@ public final class Cookies implements CookiesDefinition { private HttpServletRequest request; private HttpServletResponse response; private AttributesDefinition defaults = Attributes.empty().path( "/" ); - private CookiesDefinition.Converter converter; + 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, CookiesDefinition.Converter converter ) { + private Cookies( HttpServletRequest request, HttpServletResponse response, ConverterStrategy converter ) { this( request, response ); this.converter = converter; } @@ -234,7 +234,7 @@ public void setDefaults( AttributesDefinition defaults ) { } @Override - public Cookies withConverter( CookiesDefinition.Converter converter ) { + public Cookies withConverter( ConverterStrategy converter ) { return new Cookies( request, response, converter ); } @@ -405,5 +405,5 @@ private Attributes merge( AttributesDefinition reference ) { } } - public static abstract class Converter extends CookiesDefinition.Converter {} + public static abstract class Converter extends ConverterStrategy {} } diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index 91068d8..84aac14 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -147,14 +147,5 @@ interface CookiesDefinition { * for each cookie.
          * The returning String will be used as the cookie value. */ - CookiesDefinition withConverter( Converter converter ); - - abstract class Converter { - /** - * Determine 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; - } + CookiesDefinition withConverter( ConverterStrategy converter ); } From 513e6ebcdd1bae336c5a18bce5c2f9c5767e5c4c Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 31 May 2015 23:14:33 -0300 Subject: [PATCH 44/63] Make it possible to create custom converters without the inline syntax --- src/main/java/org/jscookie/ConverterStrategy.java | 2 +- src/main/java/org/jscookie/Cookies.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jscookie/ConverterStrategy.java b/src/main/java/org/jscookie/ConverterStrategy.java index 542060e..bc2c7ce 100644 --- a/src/main/java/org/jscookie/ConverterStrategy.java +++ b/src/main/java/org/jscookie/ConverterStrategy.java @@ -1,6 +1,6 @@ package org.jscookie; -abstract class ConverterStrategy { +interface ConverterStrategy { /** * Apply the decoding strategy of a cookie. The return will be used as the cookie value * diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 877d0d8..41364b4 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -405,5 +405,5 @@ private Attributes merge( AttributesDefinition reference ) { } } - public static abstract class Converter extends ConverterStrategy {} + public static abstract class Converter implements ConverterStrategy {} } From 0ae0adce0fece5dbb0e90d669922cc92c11eaa37 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 31 May 2015 23:22:25 -0300 Subject: [PATCH 45/63] Try to use primitives as much as possible to prevent NPE --- src/main/java/org/jscookie/AttributesDefinition.java | 2 +- src/main/java/org/jscookie/Cookies.java | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jscookie/AttributesDefinition.java b/src/main/java/org/jscookie/AttributesDefinition.java index c631b5a..87545af 100644 --- a/src/main/java/org/jscookie/AttributesDefinition.java +++ b/src/main/java/org/jscookie/AttributesDefinition.java @@ -4,5 +4,5 @@ public abstract class AttributesDefinition { abstract Expiration expires(); abstract String path(); abstract String domain(); - abstract Boolean secure(); + abstract boolean secure(); } diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 41364b4..a3e22c7 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -340,7 +340,7 @@ public static class Attributes extends AttributesDefinition { private Expiration expires; private String path; private String domain; - private Boolean secure; + private boolean secure; private Attributes() {} @@ -379,11 +379,10 @@ public Attributes domain( @Nullable String domain ) { } @Override - @Nullable - Boolean secure() { + boolean secure() { return secure; } - public Attributes secure( @Nullable Boolean secure ) { + public Attributes secure( boolean secure ) { this.secure = secure; return this; } @@ -395,12 +394,10 @@ private Attributes merge( AttributesDefinition reference ) { if ( reference.domain() != null ) { domain = reference.domain(); } - if ( reference.secure() != null ) { - secure = reference.secure(); - } if ( reference.expires() != null ) { expires = reference.expires(); } + secure = reference.secure(); return this; } } From 38c4902a702ce8e6660ca536595deaad25440863 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 31 May 2015 23:25:23 -0300 Subject: [PATCH 46/63] Let's not add an unnecessary dependency I am going to assume every public API can be nullable --- pom.xml | 5 ----- src/main/java/org/jscookie/Cookies.java | 11 +++-------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index 81311bd..ffb8b14 100644 --- a/pom.xml +++ b/pom.xml @@ -142,11 +142,6 @@ joda-time 2.7
          - - org.eclipse.jdt - org.eclipse.jdt.annotation - 2.0.0 - com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index a3e22c7..e12055e 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -15,8 +15,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jdt.annotation.Nullable; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -349,31 +347,28 @@ public static Attributes empty() { } @Override - @Nullable Expiration expires() { return expires; } - public Attributes expires( @Nullable Expiration expires ) { + public Attributes expires( Expiration expires ) { this.expires = expires; return this; } @Override - @Nullable String path() { return path; } - public Attributes path( @Nullable String path ) { + public Attributes path( String path ) { this.path = path; return this; } @Override - @Nullable String domain() { return domain; } - public Attributes domain( @Nullable String domain ) { + public Attributes domain( String domain ) { this.domain = domain; return this; } From e82676f9b8cbc06feda4aed526913f5ff18da13b Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 31 May 2015 23:35:58 -0300 Subject: [PATCH 47/63] Use a static factory method to instantiate from servlet --- src/main/java/org/jscookie/Cookies.java | 8 ++++---- .../test/integration/encoding/EncodingServlet.java | 2 +- .../java/org/jscookie/test/unit/CookiesConverterTest.java | 2 +- .../java/org/jscookie/test/unit/CookiesDecodingTest.java | 2 +- .../java/org/jscookie/test/unit/CookiesEncodingTest.java | 2 +- .../java/org/jscookie/test/unit/CookiesJSONReadTest.java | 2 +- .../java/org/jscookie/test/unit/CookiesJSONWriteTest.java | 2 +- src/test/java/org/jscookie/test/unit/CookiesReadTest.java | 2 +- .../java/org/jscookie/test/unit/CookiesWriteTest.java | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index e12055e..48a5aca 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -30,13 +30,13 @@ public final class Cookies implements CookiesDefinition { private static ResourceBundle lStrings = ResourceBundle.getBundle( LSTRING_FILE ); private Cookies( HttpServletRequest request, HttpServletResponse response, ConverterStrategy converter ) { - this( request, response ); + this.request = request; + this.response = response; this.converter = converter; } - public Cookies( HttpServletRequest request, HttpServletResponse response ) { - this.request = request; - this.response = response; + public static Cookies initFromServlet( HttpServletRequest request, HttpServletResponse response ) { + return new Cookies( request, response, null ); } @Override diff --git a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java index 326566d..ff823c2 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java @@ -23,7 +23,7 @@ public void doGet( HttpServletRequest request, HttpServletResponse response ) System.out.println( "--------------------" ); System.out.println( "Testing: " + name ); - Cookies cookies = new Cookies( request, response ); + Cookies cookies = Cookies.initFromServlet( request, response ); String value = cookies.get( name ); if ( value == null ) { diff --git a/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java b/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java index bfcbaee..5b3dcae 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java @@ -21,7 +21,7 @@ public class CookiesConverterTest extends BaseTest { @Before public void before() { - cookies = new Cookies( request, response ); + cookies = Cookies.initFromServlet( request, response ); } @Test diff --git a/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java index 8bb8205..45cec1c 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java @@ -17,7 +17,7 @@ public class CookiesDecodingTest extends BaseTest { @Before public void before() { - cookies = new Cookies( request, response ); + cookies = Cookies.initFromServlet( request, response ); } @Test diff --git a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java index 9aa44a7..2bfb586 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -18,7 +18,7 @@ public class CookiesEncodingTest extends BaseTest { @Before public void before() { - cookies = new Cookies( request, response ); + cookies = Cookies.initFromServlet( request, response ); } @Test diff --git a/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java b/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java index 0d7c024..1a633e6 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java @@ -23,7 +23,7 @@ public class CookiesJSONReadTest extends BaseTest { @Before public void before() { - cookies = new Cookies( request, response ); + cookies = Cookies.initFromServlet( request, response ); } @Test diff --git a/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java index 71b1ba9..c857c0c 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java @@ -22,7 +22,7 @@ public class CookiesJSONWriteTest extends BaseTest { @Before public void before() { - cookies = new Cookies( request, response ); + cookies = Cookies.initFromServlet( request, response ); } @Test diff --git a/src/test/java/org/jscookie/test/unit/CookiesReadTest.java b/src/test/java/org/jscookie/test/unit/CookiesReadTest.java index f2cd1bc..b0773e2 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesReadTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesReadTest.java @@ -19,7 +19,7 @@ public class CookiesReadTest extends BaseTest { @Before public void before() { - cookies = new Cookies( request, response ); + cookies = Cookies.initFromServlet( request, response ); } @Test diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index 47718cb..074e278 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -19,7 +19,7 @@ public class CookiesWriteTest extends BaseTest { @Before public void before() { - cookies = new Cookies( request, response ); + cookies = Cookies.initFromServlet( request, response ); } @Test From 06006dcabc535fdcfd73fe0d809de16063bacb1a Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 4 Jun 2015 17:41:13 -0300 Subject: [PATCH 48/63] Make it possible to change each specific defaults It was allowing only to set a given "defaults" instance but in this case we need to allow changing the current defaults properties instead. --- .../org/jscookie/AttributesDefinition.java | 6 +++++- src/main/java/org/jscookie/Cookies.java | 21 +++++++++++-------- .../java/org/jscookie/CookiesDefinition.java | 4 ++-- .../jscookie/test/unit/CookiesWriteTest.java | 12 +++++------ 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/jscookie/AttributesDefinition.java b/src/main/java/org/jscookie/AttributesDefinition.java index 87545af..83b4f0c 100644 --- a/src/main/java/org/jscookie/AttributesDefinition.java +++ b/src/main/java/org/jscookie/AttributesDefinition.java @@ -1,8 +1,12 @@ 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(); - abstract boolean secure(); + public abstract AttributesDefinition secure( Boolean secure ); + abstract Boolean secure(); } diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 48a5aca..5ea3149 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -224,11 +224,8 @@ public void remove( String name ) { } @Override - public void setDefaults( AttributesDefinition defaults ) { - if ( defaults == null ) { - throw new IllegalArgumentException(); - } - this.defaults = defaults; + public AttributesDefinition defaults() { + return this.defaults; } @Override @@ -338,7 +335,7 @@ public static class Attributes extends AttributesDefinition { private Expiration expires; private String path; private String domain; - private boolean secure; + private Boolean secure; private Attributes() {} @@ -350,6 +347,7 @@ public static Attributes empty() { Expiration expires() { return expires; } + @Override public Attributes expires( Expiration expires ) { this.expires = expires; return this; @@ -359,6 +357,7 @@ public Attributes expires( Expiration expires ) { String path() { return path; } + @Override public Attributes path( String path ) { this.path = path; return this; @@ -368,16 +367,18 @@ public Attributes path( String path ) { String domain() { return domain; } + @Override public Attributes domain( String domain ) { this.domain = domain; return this; } @Override - boolean secure() { + Boolean secure() { return secure; } - public Attributes secure( boolean secure ) { + @Override + public Attributes secure( Boolean secure ) { this.secure = secure; return this; } @@ -392,7 +393,9 @@ private Attributes merge( AttributesDefinition reference ) { if ( reference.expires() != null ) { expires = reference.expires(); } - secure = reference.secure(); + if ( reference.secure() != null ) { + secure = reference.secure(); + } return this; } } diff --git a/src/main/java/org/jscookie/CookiesDefinition.java b/src/main/java/org/jscookie/CookiesDefinition.java index 84aac14..46872be 100644 --- a/src/main/java/org/jscookie/CookiesDefinition.java +++ b/src/main/java/org/jscookie/CookiesDefinition.java @@ -136,9 +136,9 @@ interface CookiesDefinition { void remove( String name ); /** - * Change the default attributes of this instance + * Retrieve the default attributes of this instance */ - void setDefaults( AttributesDefinition attributes ); + AttributesDefinition defaults(); /** * Create a new instance of the api that overrides the default decoding implementation
          diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index 074e278..e63eafb 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -28,7 +28,7 @@ public void simple_write() { ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); Mockito.verify( response ).addCookie( argument.capture() ); - + Cookie actual = argument.getValue(); Assert.assertEquals( "c", actual.getName() ); Assert.assertEquals( "v", actual.getValue() ); @@ -40,12 +40,11 @@ public void simple_write() { @Test public void simple_write_with_default_attributes() { - cookies.setDefaults(Cookies.Attributes.empty() + cookies.defaults() .path( "/" ) .domain( "site.com" ) .secure( true ) - .expires( Expiration.days( 1 ) ) - ); + .expires( Expiration.days( 1 ) ); cookies.set( "c", "v" ); ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); @@ -83,10 +82,9 @@ public void simple_write_with_attributes() { @Test public void simple_write_overriding_default_attributes() { - cookies.setDefaults(Cookies.Attributes.empty() + cookies.defaults() .path( "/path/" ) - .secure( true ) - ); + .secure( true ); cookies.set( "c", "v", Cookies.Attributes.empty() .path( "/" ) ); From 5226f6356ddf5b9c9a90a4ecdf5fd428901ca3b4 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 4 Jun 2015 17:48:27 -0300 Subject: [PATCH 49/63] This is ignored by bower https://github.com/bower/bower.json-spec/commit/a325da3d79baab018c572d75dc1781b12322f6cd --- bower.json | 1 - 1 file changed, 1 deletion(-) diff --git a/bower.json b/bower.json index 6447c61..db3a9c9 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,5 @@ { "name": "java-cookie", - "version": "1.0.0", "devDependencies": { "js-cookie": "integration-api" } From 2bef80fab44e9154ffbff830cd7076bd04c57160 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 4 Jun 2015 19:06:34 -0300 Subject: [PATCH 50/63] If the path attribute is removed, use the whole site. To be consistent with js-cookie behavior. --- src/main/java/org/jscookie/Cookies.java | 12 ++++++++---- .../org/jscookie/test/unit/CookiesWriteTest.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 5ea3149..156bf5c 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -22,7 +22,7 @@ public final class Cookies implements CookiesDefinition { private HttpServletRequest request; private HttpServletResponse response; - private AttributesDefinition defaults = Attributes.empty().path( "/" ); + private AttributesDefinition defaults = Attributes.empty(); private ConverterStrategy converter; private ObjectMapper mapper = new ObjectMapper(); @@ -117,7 +117,7 @@ public synchronized void set( String name, String value, AttributesDefinition at String encodedValue = encode( value ); Cookie cookie = new Cookie( encodedName, encodedValue ); - attributes = extend( defaults, attributes ); + attributes = extend( Attributes.empty().path( "/" ), defaults, attributes ); Expiration expires = attributes.expires(); if ( expires != null ) { @@ -233,8 +233,12 @@ public Cookies withConverter( ConverterStrategy converter ) { return new Cookies( request, response, converter ); } - private Attributes extend( AttributesDefinition a, AttributesDefinition b ) { - return Attributes.empty().merge( a ).merge( b ); + private Attributes extend( AttributesDefinition... mergeables ) { + Attributes result = Attributes.empty(); + for ( AttributesDefinition mergeable : mergeables ) { + result.merge( mergeable ); + } + return result; } private String encode( String plain ) { diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index e63eafb..db159a4 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -98,4 +98,19 @@ public void simple_write_overriding_default_attributes() { Assert.assertEquals( "/", actual.getPath() ); Assert.assertEquals( "should consider default if not overriden", true, actual.getSecure() ); } + + @Test + public void removing_default_path_should_fallback_to_whole_site() { + cookies.defaults() + .path( null ); + cookies.set( "c", "v" ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "c", actual.getName() ); + Assert.assertEquals( "v", actual.getValue() ); + Assert.assertEquals( "should fallback to whole site if removed", "/", actual.getPath() ); + } } From 7b6a4dc7d4ce9040afb0388168003e98e2486c17 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 4 Jun 2015 19:19:37 -0300 Subject: [PATCH 51/63] If sending an empty string to the path make it a session cookie To make it consistent with js-cookie behavior. --- src/main/java/org/jscookie/Cookies.java | 2 +- .../org/jscookie/test/unit/CookiesWriteTest.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 156bf5c..d06bf48 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -125,7 +125,7 @@ public synchronized void set( String name, String value, AttributesDefinition at } String path = attributes.path(); - if ( path != null ) { + if ( path != null && !path.isEmpty() ) { cookie.setPath( path ); } diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index db159a4..590c055 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -113,4 +113,19 @@ public void removing_default_path_should_fallback_to_whole_site() { Assert.assertEquals( "v", actual.getValue() ); Assert.assertEquals( "should fallback to whole site if removed", "/", actual.getPath() ); } + + @Test + public void should_not_write_the_path_attribute_if_set_as_an_empty_string() { + cookies.defaults() + .path( "" ); + cookies.set( "c", "v" ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "c", actual.getName() ); + Assert.assertEquals( "v", actual.getValue() ); + Assert.assertEquals( "should not send the path", null, actual.getPath() ); + } } From a3b5dac6efdc0a673d21fc775f20d9eac69a81b6 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Thu, 4 Jun 2015 23:56:06 -0300 Subject: [PATCH 52/63] This dependency does not exist anymore --- .../jscookie/test/integration/encoding/CookiesEncodingIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java index 564aab9..ca731a0 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java +++ b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java @@ -49,7 +49,6 @@ public static Archive createDeployment() { .loadPomFromFile( "pom.xml" ) .resolve( "joda-time:joda-time", - "org.eclipse.jdt:org.eclipse.jdt.annotation", "com.fasterxml.jackson.core:jackson-databind" ) .withTransitivity() From c157e41f3b1c0412577333cd7b92083976f02ba6 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Fri, 5 Jun 2015 23:57:53 -0300 Subject: [PATCH 53/63] Read the cookies from the header to workaround some problems Namely http://stackoverflow.com/q/30677783/1400037 --- src/main/java/org/jscookie/Cookies.java | 54 ++++++++++--------- .../test/unit/CookiesConverterTest.java | 8 ++- .../test/unit/CookiesDecodingTest.java | 10 +--- .../test/unit/CookiesJSONReadTest.java | 26 +++------ .../jscookie/test/unit/CookiesReadTest.java | 20 +++---- .../jscookie/test/unit/utils/BaseTest.java | 8 --- 6 files changed, 51 insertions(+), 75 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index d06bf48..c2c9203 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -45,19 +45,17 @@ public synchronized String get( String name ) { throw new IllegalArgumentException( lStrings.getString( "err.cookie_name_blank" ) ); } - Cookie[] cookies = request.getCookies(); - if ( cookies == null ) { + String cookieHeader = request.getHeader( "cookie" ); + if ( cookieHeader == null ) { return null; } - for ( Cookie cookie : cookies ) { - String decodedName = decode( cookie.getName() ); - + Map cookies = getCookies( cookieHeader ); + for ( String decodedName : cookies.keySet() ) { if ( !name.equals( decodedName ) ) { continue; } - - return decodeValue( cookie, decodedName ); + return cookies.get( decodedName ); } return null; @@ -87,18 +85,12 @@ public T get( String name, TypeReference typeRef ) throws CookieParseExce public Map get() { Map result = new HashMap(); - Cookie[] cookies = request.getCookies(); - if ( cookies == null ) { - return null; + String cookieHeader = request.getHeader( "cookie" ); + if ( cookieHeader == null ) { + return result; } - for ( Cookie cookie : cookies ) { - String decodedName = decode( cookie.getName() ); - String decodedValue = decodeValue( cookie, decodedName ); - result.put( decodedName, decodedValue ); - } - - return result; + return getCookies( cookieHeader ); } @Override @@ -296,11 +288,8 @@ private String decode( String encoded ) { String decoded = encoded; Pattern pattern = Pattern.compile( "(%[0-9A-Z]{2})+" ); Matcher matcher = pattern.matcher( encoded ); - if ( !matcher.matches() ) { - return decoded; - } - for ( int groupIndex = 0; groupIndex < matcher.groupCount(); groupIndex++ ) { - String encodedChar = matcher.group( groupIndex ); + 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++ ) { @@ -317,24 +306,39 @@ private String decode( String encoded ) { return decoded; } - private String decodeValue( Cookie cookie, String decodedName ) { + private String decodeValue( String plainValue, String decodedName ) { String decodedValue = null; if ( converter != null ) { try { - decodedValue = converter.convert( cookie.getValue(), decodedName ); + decodedValue = converter.convert( plainValue, decodedName ); } catch ( ConverterException e ) { e.printStackTrace(); } } if ( decodedValue == null ) { - decodedValue = decode( cookie.getValue() ); + decodedValue = decode( plainValue ); } 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; diff --git a/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java b/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java index 5b3dcae..9e7d922 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesConverterTest.java @@ -3,7 +3,6 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; -import javax.servlet.http.Cookie; import org.jscookie.ConverterException; import org.jscookie.Cookies; @@ -26,10 +25,9 @@ public void before() { @Test public void should_be_able_to_conditionally_decode_a_single_malformed_cookie() { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "escaped", "%u5317" ), - new Cookie( "encoded", "%E4%BA%AC" ) - }); + 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 { diff --git a/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java index 45cec1c..0b94f80 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesDecodingTest.java @@ -1,7 +1,5 @@ package org.jscookie.test.unit; -import javax.servlet.http.Cookie; - import org.jscookie.Cookies; import org.jscookie.test.unit.utils.BaseTest; import org.junit.Assert; @@ -22,9 +20,7 @@ public void before() { @Test public void character_not_allowed_in_name_and_value() { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "%3B", "%3B" ) - }); + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "%3B=%3B" ); String actual = cookies.get( ";" ); String expected = ";"; Assert.assertEquals( expected, actual ); @@ -32,9 +28,7 @@ public void character_not_allowed_in_name_and_value() { @Test public void character_with_3_bytes() { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie("c", "%E4%BA%AC") - }); + 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/CookiesJSONReadTest.java b/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java index 1a633e6..771dca8 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesJSONReadTest.java @@ -2,8 +2,6 @@ import java.util.List; -import javax.servlet.http.Cookie; - import org.jscookie.CookieParseException; import org.jscookie.Cookies; import org.jscookie.test.unit.utils.BaseTest; @@ -28,9 +26,7 @@ public void before() { @Test public void read_int_type() throws CookieParseException { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "c", "1" ) - }); + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=1" ); String actual = cookies.get( "c" ); String expected = "1"; @@ -43,9 +39,7 @@ public void read_int_type() throws CookieParseException { @Test public void read_boolean_type() throws CookieParseException { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "c", "true" ) - }); + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=true" ); String actual = cookies.get( "c" ); String expected = "true"; @@ -58,9 +52,7 @@ public void read_boolean_type() throws CookieParseException { @Test public void read_JSON_array_with_string() throws CookieParseException { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "c", "[\"v\"]" ) - }); + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=[%22v%22]" ); String actual = cookies.get( "c" ); String expected = "[\"v\"]"; @@ -73,9 +65,7 @@ public void read_JSON_array_with_string() throws CookieParseException { @Test public void read_custom_type_with_string_prop() throws CookieParseException { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "c", "{\"property\":\"v\"}" ) - }); + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c={%22property%22:%22v%22}" ); String actual = cookies.get( "c" ); String expected = "{\"property\":\"v\"}"; @@ -88,9 +78,7 @@ public void read_custom_type_with_string_prop() throws CookieParseException { @Test public void read_custom_type_with_boolean_prop() throws CookieParseException { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "c", "{\"property\":true}" ) - }); + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c={%22property%22:true}" ); String actual = cookies.get( "c" ); String expected = "{\"property\":true}"; @@ -103,9 +91,7 @@ public void read_custom_type_with_boolean_prop() throws CookieParseException { @Test public void read_custom_type_with_number_prop() throws CookieParseException { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "c", "{\"property\":1}" ) - }); + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c={%22property%22:1}" ); String actual = cookies.get( "c" ); String expected = "{\"property\":1}"; diff --git a/src/test/java/org/jscookie/test/unit/CookiesReadTest.java b/src/test/java/org/jscookie/test/unit/CookiesReadTest.java index b0773e2..5c67c63 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesReadTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesReadTest.java @@ -2,8 +2,6 @@ import java.util.Map; -import javax.servlet.http.Cookie; - import org.jscookie.Cookies; import org.jscookie.test.unit.utils.BaseTest; import org.junit.Assert; @@ -24,9 +22,7 @@ public void before() { @Test public void simple_value() { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "c", "v" ) - }); + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=v" ); String actual = cookies.get( "c" ); String expected = "v"; Assert.assertEquals( expected, actual ); @@ -34,10 +30,7 @@ public void simple_value() { @Test public void read_all() { - Mockito.when( request.getCookies() ).thenReturn(new Cookie[] { - new Cookie( "c", "v" ), - new Cookie( "foo", "bar" ) - }); + Mockito.when( request.getHeader( "cookie" ) ).thenReturn( "c=v; foo=bar" ); Map result = cookies.get(); String actual = result.get( "c" ); @@ -48,4 +41,13 @@ public void read_all() { 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/utils/BaseTest.java b/src/test/java/org/jscookie/test/unit/utils/BaseTest.java index 7d14750..705111c 100644 --- a/src/test/java/org/jscookie/test/unit/utils/BaseTest.java +++ b/src/test/java/org/jscookie/test/unit/utils/BaseTest.java @@ -1,19 +1,11 @@ package org.jscookie.test.unit.utils; -import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.junit.Before; import org.mockito.Mock; -import org.mockito.Mockito; public class BaseTest { protected @Mock HttpServletRequest request; protected @Mock HttpServletResponse response; - - @Before - public void before() { - Mockito.when( request.getCookies() ).thenReturn( new Cookie[]{} ); - } } From bcd45b10108fa13efa64d28cc56ec2cd4727ab06 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sat, 6 Jun 2015 00:34:06 -0300 Subject: [PATCH 54/63] Using encode/decode naming makes it more understandable --- src/main/java/org/jscookie/Cookies.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index c2c9203..35d6703 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -233,10 +233,10 @@ private Attributes extend( AttributesDefinition... mergeables ) { return result; } - private String encode( String plain ) { - String encoded = plain; - for ( int i = 0; i < plain.length(); i++ ) { - Character character = plain.charAt( i ); + private String encode( String decoded ) { + String encoded = decoded; + for ( int i = 0; i < decoded.length(); i++ ) { + Character character = decoded.charAt( i ); boolean isDigit = Character.isDigit( character ); if ( isDigit ) { @@ -306,19 +306,19 @@ private String decode( String encoded ) { return decoded; } - private String decodeValue( String plainValue, String decodedName ) { + private String decodeValue( String encodedValue, String decodedName ) { String decodedValue = null; if ( converter != null ) { try { - decodedValue = converter.convert( plainValue, decodedName ); + decodedValue = converter.convert( encodedValue, decodedName ); } catch ( ConverterException e ) { e.printStackTrace(); } } if ( decodedValue == null ) { - decodedValue = decode( plainValue ); + decodedValue = decode( encodedValue ); } return decodedValue; From 02beea36cc6f320c20c9b225db2d9a914defd413 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sat, 6 Jun 2015 00:56:54 -0300 Subject: [PATCH 55/63] Some characters in the cookie-value don't need to be encoded --- src/main/java/org/jscookie/Cookies.java | 42 ++++++++++++++++++- .../test/unit/CookiesEncodingTest.java | 11 +++++ .../test/unit/CookiesJSONWriteTest.java | 8 ++-- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 35d6703..51dd6d6 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -5,9 +5,11 @@ 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; @@ -106,7 +108,7 @@ public synchronized void set( String name, String value, AttributesDefinition at } String encodedName = encode( name ); - String encodedValue = encode( value ); + String encodedValue = encodeValue( value ); Cookie cookie = new Cookie( encodedName, encodedValue ); attributes = extend( Attributes.empty().path( "/" ), defaults, attributes ); @@ -234,6 +236,10 @@ private Attributes extend( AttributesDefinition... mergeables ) { } 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(); i++ ) { Character character = decoded.charAt( i ); @@ -263,6 +269,10 @@ private String encode( String decoded ) { continue; } + if ( exceptions.contains( character ) ) { + continue; + } + try { CharArrayWriter hexSequence = new CharArrayWriter(); byte[] bytes = character.toString().getBytes( StandardCharsets.UTF_8.name() ); @@ -306,6 +316,36 @@ private String decode( String encoded ) { return decoded; } + private String encodeValue( String decodedValue ) { + Set exceptions = new HashSet<>(); + for ( int i = 0; i < decodedValue.length(); i++ ) { + char character = decodedValue.charAt( i ); + boolean isIgnorable = false; + + if ( character == '/' || character == ':' ) { + isIgnorable = true; + } + + if ( character >= '<' && character <= '@' ) { + isIgnorable = true; + } + + if ( character == '[' || character == ']' ) { + isIgnorable = true; + } + + if ( character == '{' || character == '}' ) { + isIgnorable = true; + } + + if ( isIgnorable ) { + exceptions.add( character ); + } + } + + return encode( decodedValue, exceptions ); + } + private String decodeValue( String encodedValue, String decodedName ) { String decodedValue = null; diff --git a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java index 2bfb586..12a080c 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -64,4 +64,15 @@ public void character_with_3_bytes() { Cookie actual = argument.getValue(); Assert.assertEquals( "%E4%BA%AC", actual.getValue() ); } + + @Test + public void characters_allowed_in_cookie_value() { + cookies.set( "c", "/:<=>?@[]{}" ); + + ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); + Mockito.verify( response ).addCookie( argument.capture() ); + + Cookie actual = argument.getValue(); + Assert.assertEquals( "/:<=>?@[]{}", actual.getValue() ); + } } diff --git a/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java index c857c0c..3a97f6f 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java @@ -55,7 +55,7 @@ public void write_JSON_array_with_string() throws CookieSerializationException { Mockito.verify( response ).addCookie( argument.capture() ); Cookie actual = argument.getValue(); - Assert.assertEquals( "%5B%22v%22%5D", actual.getValue() ); + Assert.assertEquals( "[%22v%22]", actual.getValue() ); } @Test @@ -66,7 +66,7 @@ public void write_custom_type_with_string_prop() throws CookieSerializationExcep Mockito.verify( response ).addCookie( argument.capture() ); Cookie actual = argument.getValue(); - Assert.assertEquals( "%7B%22property%22%3A%22v%22%7D", actual.getValue() ); + Assert.assertEquals( "{%22property%22:%22v%22}", actual.getValue() ); } @Test @@ -77,7 +77,7 @@ public void write_custom_type_with_boolean_prop() throws CookieSerializationExce Mockito.verify( response ).addCookie( argument.capture() ); Cookie actual = argument.getValue(); - Assert.assertEquals( "%7B%22property%22%3Atrue%7D", actual.getValue() ); + Assert.assertEquals( "{%22property%22:true}", actual.getValue() ); } @Test @@ -88,7 +88,7 @@ public void write_custom_type_with_number_prop() throws CookieSerializationExcep Mockito.verify( response ).addCookie( argument.capture() ); Cookie actual = argument.getValue(); - Assert.assertEquals( "%7B%22property%22%3A1%7D", actual.getValue() ); + Assert.assertEquals( "{%22property%22:1}", actual.getValue() ); } class CustomTypeString implements CookieValue { From 7cbdf97fd03c36706cb80577221c98b73da0a451 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sat, 6 Jun 2015 23:32:16 -0300 Subject: [PATCH 56/63] Use .addHeader to fix quoted values being sent using response.addCookie --- src/main/java/org/jscookie/Cookies.java | 23 ++++--- .../test/unit/CookiesEncodingTest.java | 43 ++---------- .../test/unit/CookiesJSONWriteTest.java | 46 ++----------- .../jscookie/test/unit/CookiesWriteTest.java | 66 +++---------------- 4 files changed, 37 insertions(+), 141 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 51dd6d6..a6f4bfc 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -13,7 +13,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -110,30 +109,38 @@ public synchronized void set( String name, String value, AttributesDefinition at String encodedName = encode( name ); String encodedValue = encodeValue( value ); - Cookie cookie = new Cookie( encodedName, encodedValue ); + StringBuilder header = new StringBuilder(); + header.append( encodedName ); + header.append( '=' ); + header.append( encodedValue ); + attributes = extend( Attributes.empty().path( "/" ), defaults, attributes ); Expiration expires = attributes.expires(); if ( expires != null ) { - cookie.setMaxAge( expires.toSecondsFromNow() ); + // TODO } String path = attributes.path(); if ( path != null && !path.isEmpty() ) { - cookie.setPath( path ); + header.append( "; Path=" + path ); } String domain = attributes.domain(); if ( domain != null ) { - cookie.setDomain( domain ); + header.append( "; Domain=" + domain ); } Boolean secure = attributes.secure(); - if ( secure != null ) { - cookie.setSecure( secure ); + if ( Boolean.TRUE.equals( secure ) ) { + header.append( "; Secure" ); + } + + if ( response.isCommitted() ) { + return; } - response.addCookie( cookie ); + response.addHeader( "Set-Cookie", header.toString() ); } @Override diff --git a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java index 12a080c..6dc1382 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -1,14 +1,10 @@ package org.jscookie.test.unit; -import javax.servlet.http.Cookie; - 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.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; @@ -24,13 +20,7 @@ public void before() { @Test public void character_not_allowed_in_name_and_value() { cookies.set( ";,\\\" ", ";,\\\" " ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "%3B%2C%5C%22%20", actual.getName() ); - Assert.assertEquals( "%3B%2C%5C%22%20", actual.getValue() ); + Mockito.verify( response ).addHeader( "Set-Cookie", "%3B%2C%5C%22%20=%3B%2C%5C%22%20; Path=/" ); } @Test @@ -39,40 +29,21 @@ public void characters_allowed_in_name_and_value() { "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~", "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~" ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( - "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~", - actual.getValue() - ); - Assert.assertEquals( - "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~", - actual.getName() + Mockito.verify( response ).addHeader( + "Set-Cookie", + "!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~=!#$&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~; Path=/" ); } @Test - public void character_with_3_bytes() { + public void character_with_3_bytes_in_value() { cookies.set( "c", "京" ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "%E4%BA%AC", actual.getValue() ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=%E4%BA%AC; Path=/" ); } @Test public void characters_allowed_in_cookie_value() { cookies.set( "c", "/:<=>?@[]{}" ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "/:<=>?@[]{}", actual.getValue() ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=/:<=>?@[]{}; Path=/" ); } } diff --git a/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java index 3a97f6f..2056a11 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesJSONWriteTest.java @@ -2,17 +2,13 @@ import java.util.Arrays; -import javax.servlet.http.Cookie; - import org.jscookie.CookieSerializationException; import org.jscookie.CookieValue; 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.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; @@ -28,67 +24,37 @@ public void before() { @Test public void write_int_type() throws CookieSerializationException { cookies.set( "c", 1 ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "1", actual.getValue() ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=1; Path=/" ); } @Test public void write_boolean_type() throws CookieSerializationException { cookies.set( "c", true ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "true", actual.getValue() ); + 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" ) ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "[%22v%22]", actual.getValue() ); + 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" ) ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "{%22property%22:%22v%22}", actual.getValue() ); + 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 ) ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "{%22property%22:true}", actual.getValue() ); + 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 ) ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "{%22property%22:1}", actual.getValue() ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c={%22property%22:1}; Path=/" ); } class CustomTypeString implements CookieValue { diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index 590c055..f6b0829 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -1,15 +1,11 @@ package org.jscookie.test.unit; -import javax.servlet.http.Cookie; - import org.jscookie.Cookies; import org.jscookie.Expiration; 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.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; @@ -25,17 +21,7 @@ public void before() { @Test public void simple_write() { cookies.set( "c", "v" ); - - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "c", actual.getName() ); - Assert.assertEquals( "v", actual.getValue() ); - Assert.assertEquals( "/", actual.getPath() ); - Assert.assertEquals( null, actual.getDomain() ); - Assert.assertEquals( false, actual.getSecure() ); - Assert.assertEquals( -1, actual.getMaxAge() ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/" ); } @Test @@ -47,16 +33,8 @@ public void simple_write_with_default_attributes() { .expires( Expiration.days( 1 ) ); cookies.set( "c", "v" ); - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "c", actual.getName() ); - Assert.assertEquals( "v", actual.getValue() ); - Assert.assertEquals( "/", actual.getPath() ); - Assert.assertEquals( "site.com", actual.getDomain() ); - Assert.assertEquals( true, actual.getSecure() ); - Assert.assertEquals( 86400, actual.getMaxAge() ); + // TODO Add expires + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; Domain=site.com; Secure" ); } @Test @@ -68,16 +46,8 @@ public void simple_write_with_attributes() { .expires( Expiration.days( 1 ) ) ); - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "c", actual.getName() ); - Assert.assertEquals( "v", actual.getValue() ); - Assert.assertEquals( "/", actual.getPath() ); - Assert.assertEquals( "example.com", actual.getDomain() ); - Assert.assertEquals( true, actual.getSecure() ); - Assert.assertEquals( 86400, actual.getMaxAge() ); + // TODO expires + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; Domain=example.com; Secure" ); } @Test @@ -89,14 +59,8 @@ public void simple_write_overriding_default_attributes() { .path( "/" ) ); - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "c", actual.getName() ); - Assert.assertEquals( "v", actual.getValue() ); - Assert.assertEquals( "/", actual.getPath() ); - Assert.assertEquals( "should consider default if not overriden", true, actual.getSecure() ); + // Should consider default secure if not overriden + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; Secure" ); } @Test @@ -105,13 +69,7 @@ public void removing_default_path_should_fallback_to_whole_site() { .path( null ); cookies.set( "c", "v" ); - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "c", actual.getName() ); - Assert.assertEquals( "v", actual.getValue() ); - Assert.assertEquals( "should fallback to whole site if removed", "/", actual.getPath() ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/" ); } @Test @@ -120,12 +78,6 @@ public void should_not_write_the_path_attribute_if_set_as_an_empty_string() { .path( "" ); cookies.set( "c", "v" ); - ArgumentCaptor argument = ArgumentCaptor.forClass( Cookie.class ); - Mockito.verify( response ).addCookie( argument.capture() ); - - Cookie actual = argument.getValue(); - Assert.assertEquals( "c", actual.getName() ); - Assert.assertEquals( "v", actual.getValue() ); - Assert.assertEquals( "should not send the path", null, actual.getPath() ); + Mockito.verify( response ).addHeader( "Set-Cookie", "c=v" ); } } From cc9d15e76f6f638854d00a354b62e704688524d7 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 7 Jun 2015 00:08:30 -0300 Subject: [PATCH 57/63] Implement expires format --- src/main/java/org/jscookie/Cookies.java | 10 +++--- src/main/java/org/jscookie/Expiration.java | 32 +++++++------------ .../jscookie/test/unit/CookiesWriteTest.java | 17 +++++++--- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index a6f4bfc..baffc0f 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -116,16 +116,16 @@ public synchronized void set( String name, String value, AttributesDefinition at attributes = extend( Attributes.empty().path( "/" ), defaults, attributes ); - Expiration expires = attributes.expires(); - if ( expires != null ) { - // TODO - } - 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 ); diff --git a/src/main/java/org/jscookie/Expiration.java b/src/main/java/org/jscookie/Expiration.java index 2adbc94..cc6e99b 100644 --- a/src/main/java/org/jscookie/Expiration.java +++ b/src/main/java/org/jscookie/Expiration.java @@ -1,23 +1,23 @@ package org.jscookie; import java.util.Date; +import java.util.Locale; import org.joda.time.DateTime; -import org.joda.time.Seconds; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; public final class Expiration { - private final DateTime dateTime; - private final Integer days; + 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.dateTime = dateTime; - this.days = null; - } - private Expiration( Integer days ) { - this.dateTime = null; - this.days = days; + this.date = dateTime; } public static Expiration days( int days ) { - return new Expiration( days ); + DateTime withDays = new DateTime().plusDays( days ); + return new Expiration( withDays ); } public static Expiration date( DateTime dateTime ) { if ( dateTime == null ) { @@ -32,15 +32,7 @@ public static Expiration date( Date date ) { DateTime dateTime = new DateTime( date ); return new Expiration( dateTime ); } - int toSecondsFromNow() { - int seconds = 0; - if ( days != null ) { - int oneDayInSeconds = 86400; - seconds = oneDayInSeconds * days; - } else if ( dateTime != null ) { - Seconds period = Seconds.secondsBetween( DateTime.now(), dateTime ); - seconds = period.getSeconds(); - } - return seconds; + String toExpiresString() { + return date.toString( EXPIRES_FORMAT ); } } diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index f6b0829..97dfe6b 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -1,6 +1,8 @@ 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; @@ -29,11 +31,9 @@ public void simple_write_with_default_attributes() { cookies.defaults() .path( "/" ) .domain( "site.com" ) - .secure( true ) - .expires( Expiration.days( 1 ) ); + .secure( true ); cookies.set( "c", "v" ); - // TODO Add expires Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; Domain=site.com; Secure" ); } @@ -43,10 +43,8 @@ public void simple_write_with_attributes() { .path( "/" ) .domain( "example.com" ) .secure( true ) - .expires( Expiration.days( 1 ) ) ); - // TODO expires Mockito.verify( response ).addHeader( "Set-Cookie", "c=v; Path=/; Domain=example.com; Secure" ); } @@ -80,4 +78,13 @@ public void should_not_write_the_path_attribute_if_set_as_an_empty_string() { 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" ); + } } From f0939c8db6ca5a4f394de9126ead8a7bc8e41903 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 7 Jun 2015 00:10:47 -0300 Subject: [PATCH 58/63] Test character with 3 bytes in name --- .../java/org/jscookie/test/unit/CookiesEncodingTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java index 6dc1382..74fb955 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -41,6 +41,12 @@ public void character_with_3_bytes_in_value() { 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 characters_allowed_in_cookie_value() { cookies.set( "c", "/:<=>?@[]{}" ); From 30494c6e5a501a63cb5e09ac1f62ba6a94a03149 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 7 Jun 2015 01:42:41 -0300 Subject: [PATCH 59/63] Iterate over code points to be able to encode 4 byte characters --- src/main/java/org/jscookie/Cookies.java | 55 +++++++++++-------- .../test/unit/CookiesEncodingTest.java | 6 ++ 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/jscookie/Cookies.java b/src/main/java/org/jscookie/Cookies.java index baffc0f..829db3a 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -243,46 +243,50 @@ private Attributes extend( AttributesDefinition... mergeables ) { } private String encode( String decoded ) { - return encode( decoded, new HashSet() ); + return encode( decoded, new HashSet() ); } - private String encode( String decoded, Set exceptions ) { + private String encode( String decoded, Set exceptions ) { String encoded = decoded; - for ( int i = 0; i < decoded.length(); i++ ) { - Character character = decoded.charAt( i ); + for ( int i = 0; i < decoded.length(); ) { + int codePoint = decoded.codePointAt( i ); + i += Character.charCount( codePoint ); - boolean isDigit = Character.isDigit( character ); + boolean isDigit = codePoint >= codePoint( "0" ) && codePoint <= codePoint( "9" ); if ( isDigit ) { continue; } - boolean isAsciiUppercaseLetter = character >= 'A' && character <= 'Z'; + boolean isAsciiUppercaseLetter = codePoint >= codePoint( "A" ) && codePoint <= codePoint( "Z" ); if ( isAsciiUppercaseLetter ) { continue; } - boolean isAsciiLowercaseLetter = character >= 'a' && character <= 'z'; + boolean isAsciiLowercaseLetter = codePoint >= codePoint( "a" ) && codePoint <= codePoint( "z" ); if ( isAsciiLowercaseLetter ) { continue; } boolean isAllowed = - character == '!' || character == '#' || character == '$' || - character == '&' || character == '\'' || character == '*' || - character == '+' || character == '-' || character == '.' || - character == '^' || character == '_' || character == '`' || - character == '|' || character == '~'; + 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( character ) ) { + if ( exceptions.contains( codePoint ) ) { continue; } try { + String character = new String( Character.toChars( codePoint ) ); CharArrayWriter hexSequence = new CharArrayWriter(); - byte[] bytes = character.toString().getBytes( StandardCharsets.UTF_8.name() ); + 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 ); @@ -324,35 +328,40 @@ private String decode( String encoded ) { } private String encodeValue( String decodedValue ) { - Set exceptions = new HashSet<>(); - for ( int i = 0; i < decodedValue.length(); i++ ) { - char character = decodedValue.charAt( i ); - boolean isIgnorable = false; + Set exceptions = new HashSet<>(); + for ( int i = 0; i < decodedValue.length(); ) { + int codePoint = decodedValue.codePointAt( i ); + i += Character.charCount( codePoint ); - if ( character == '/' || character == ':' ) { + boolean isIgnorable = false; + if ( codePoint == codePoint( "/" ) || codePoint == codePoint( ":") ) { isIgnorable = true; } - if ( character >= '<' && character <= '@' ) { + if ( codePoint >= codePoint( "<" ) && codePoint <= codePoint( "@" ) ) { isIgnorable = true; } - if ( character == '[' || character == ']' ) { + if ( codePoint == codePoint( "[" ) || codePoint == codePoint( "]" ) ) { isIgnorable = true; } - if ( character == '{' || character == '}' ) { + if ( codePoint == codePoint( "{" ) || codePoint == codePoint( "}" ) ) { isIgnorable = true; } if ( isIgnorable ) { - exceptions.add( character ); + 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; diff --git a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java index 74fb955..86d395c 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -47,6 +47,12 @@ public void character_with_3_bytes_in_name() { 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 characters_allowed_in_cookie_value() { cookies.set( "c", "/:<=>?@[]{}" ); From 0f8a8f4fd5d7d8bed3f4ec5e709d752f32c59097 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 7 Jun 2015 01:47:53 -0300 Subject: [PATCH 60/63] Test characters with 4 bytes mixed with single bytes --- .../java/org/jscookie/test/unit/CookiesEncodingTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java index 86d395c..dbc4d92 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesEncodingTest.java @@ -53,6 +53,12 @@ public void character_with_4_bytes_in_name() { 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", "/:<=>?@[]{}" ); From a5c455dc86abedbf8c428eff17ed41826a0ef21d Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 7 Jun 2015 02:17:28 -0300 Subject: [PATCH 61/63] Fix the retrieval of UTF-8 characters from the server With this all encoding tests pass for js-cookie, ;D --- .../integration/encoding/EncodingServlet.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java index ff823c2..69236d5 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java @@ -1,6 +1,9 @@ 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; @@ -18,7 +21,7 @@ public class EncodingServlet extends HttpServlet { @Override public void doGet( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException { - String name = request.getParameter( "name" ); + String name = getUTF8Param( "name", request ); System.out.println( "--------------------" ); System.out.println( "Testing: " + name ); @@ -39,6 +42,19 @@ public void doGet( HttpServletRequest request, HttpServletResponse response ) 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 { From 4d22947d2d514eccb86941f0748c666c3dbcaade Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 7 Jun 2015 17:34:48 -0300 Subject: [PATCH 62/63] Fix build with Debug.FALSE Let's use the Firefox driver instead of HTMLUnit, the HTMLUnit is not working properly. I am going to implement it later if necessary. --- bower.json | 2 +- .../encoding/CookiesEncodingIT.java | 22 ++----------------- .../integration/encoding/EncodingServlet.java | 4 ---- 3 files changed, 3 insertions(+), 25 deletions(-) diff --git a/bower.json b/bower.json index db3a9c9..8ac5a2a 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "java-cookie", "devDependencies": { - "js-cookie": "integration-api" + "js-cookie": "master" } } diff --git a/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java index ca731a0..e39e00c 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java +++ b/src/test/java/org/jscookie/test/integration/encoding/CookiesEncodingIT.java @@ -23,14 +23,11 @@ import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; -import org.openqa.selenium.htmlunit.HtmlUnitDriver; import org.openqa.selenium.support.PageFactory; -import com.gargoylesoftware.htmlunit.BrowserVersion; - @RunWith( Arquillian.class ) public class CookiesEncodingIT { - private static Debug debug = Debug.TRUE; + private static Debug debug = Debug.FALSE; @Deployment public static Archive createDeployment() { @@ -66,7 +63,7 @@ public static Archive createDeployment() { @RunAsClient @Test public void read_qunit_test( @ArquillianResource URL baseURL ) { - WebDriver driver = createDriver(); + WebDriver driver = new FirefoxDriver(); driver.manage().timeouts().implicitlyWait( 20, TimeUnit.SECONDS ); EncodingPageObject encoding = PageFactory.initElements( driver, EncodingPageObject.class ); @@ -78,21 +75,6 @@ public void read_qunit_test( @ArquillianResource URL baseURL ) { int actualPasses = results.getPassed(); Assert.assertEquals( "should pass all tests", expectedPasses, actualPasses ); - dispose( driver ); - } - - private WebDriver createDriver() { - if ( debug.is( true ) ) { - return new FirefoxDriver(); - } - - HtmlUnitDriver driver = new HtmlUnitDriver( BrowserVersion.CHROME ); - driver.setJavascriptEnabled( true ); - - return driver; - } - - private void dispose( WebDriver driver ) { if ( debug.is( false ) ) { driver.quit(); } diff --git a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java index 69236d5..bc7424d 100644 --- a/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java +++ b/src/test/java/org/jscookie/test/integration/encoding/EncodingServlet.java @@ -23,7 +23,6 @@ public void doGet( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException { String name = getUTF8Param( "name", request ); - System.out.println( "--------------------" ); System.out.println( "Testing: " + name ); Cookies cookies = Cookies.initFromServlet( request, response ); @@ -33,9 +32,6 @@ public void doGet( HttpServletRequest request, HttpServletResponse response ) throw new NullPointerException( "Cookie not found with name: " + name ); } - System.out.println( "Value: " + value ); - System.out.println( "--------------------" ); - cookies.set( name, value ); response.setContentType( "application/json" ); From 3ebdc768672fee5e5936e4a2453046cd252de903 Mon Sep 17 00:00:00 2001 From: Fagner Brack Date: Sun, 7 Jun 2015 18:31:53 -0300 Subject: [PATCH 63/63] Add httpOnly attribute --- .../org/jscookie/AttributesDefinition.java | 2 ++ src/main/java/org/jscookie/Cookies.java | 19 +++++++++++++++++++ .../jscookie/test/unit/CookiesWriteTest.java | 8 ++++++++ 3 files changed, 29 insertions(+) diff --git a/src/main/java/org/jscookie/AttributesDefinition.java b/src/main/java/org/jscookie/AttributesDefinition.java index 83b4f0c..4cac229 100644 --- a/src/main/java/org/jscookie/AttributesDefinition.java +++ b/src/main/java/org/jscookie/AttributesDefinition.java @@ -9,4 +9,6 @@ public abstract class AttributesDefinition { 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/Cookies.java b/src/main/java/org/jscookie/Cookies.java index 829db3a..d55807f 100644 --- a/src/main/java/org/jscookie/Cookies.java +++ b/src/main/java/org/jscookie/Cookies.java @@ -136,6 +136,11 @@ public synchronized void set( String name, String value, AttributesDefinition at header.append( "; Secure" ); } + Boolean httpOnly = attributes.httpOnly(); + if ( Boolean.TRUE.equals( httpOnly ) ) { + header.append( "; HttpOnly" ); + } + if ( response.isCommitted() ) { return; } @@ -400,6 +405,7 @@ public static class Attributes extends AttributesDefinition { private String path; private String domain; private Boolean secure; + private Boolean httpOnly; private Attributes() {} @@ -447,6 +453,16 @@ public Attributes secure( Boolean 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(); @@ -460,6 +476,9 @@ private Attributes merge( AttributesDefinition reference ) { if ( reference.secure() != null ) { secure = reference.secure(); } + if ( reference.httpOnly() != null ) { + httpOnly = reference.httpOnly(); + } return this; } } diff --git a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java index 97dfe6b..525a615 100644 --- a/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java +++ b/src/test/java/org/jscookie/test/unit/CookiesWriteTest.java @@ -87,4 +87,12 @@ public void expires_attribute() { ); 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" ); + } }