diff --git a/.rultor.yml b/.rultor.yml index 5b8d81de8..e0aadd56a 100644 --- a/.rultor.yml +++ b/.rultor.yml @@ -34,7 +34,6 @@ merge: mvn clean pdd --source=$(pwd) --verbose --file=/dev/null commanders: - - alevohin - carlosmiranda - darkled - ggajos @@ -66,4 +65,4 @@ release: mvn clean deploy -Ptakes -Psonatype --errors --settings ../settings.xml mvn clean site-deploy -Ptakes -Psite --errors --settings ../settings.xml commanders: - - yegor256 + - yegor256 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index ee0cf21ce..c84939b7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,10 @@ cache: - $HOME/.m2 script: - mvn clean install -Pqulice --errors --batch-mode +env: + global: + - MAVEN_OPTS="-Xmx256m -Dfile.encoding=UTF-8" + - JAVA_OPTS="-Xmx256m -Dfile.encoding=UTF-8" jdk: - oraclejdk8 - oraclejdk7 diff --git a/src/main/java/org/takes/facets/auth/codecs/CcBase64.java b/src/main/java/org/takes/facets/auth/codecs/CcBase64.java new file mode 100644 index 000000000..780463480 --- /dev/null +++ b/src/main/java/org/takes/facets/auth/codecs/CcBase64.java @@ -0,0 +1,73 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015 Yegor Bugayenko + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.takes.facets.auth.codecs; + +import java.io.IOException; +import lombok.EqualsAndHashCode; +import org.takes.facets.auth.Identity; + +/** + * Base64 codec. + * + *

The class is immutable and thread-safe. + * + * @author Igor Khvostenkov (ikhvostenkov@gmail.com) + * @version $Id$ + * @since 0.13 + */ +@EqualsAndHashCode +public final class CcBase64 implements Codec { + /** + * Original codec. + */ + private final transient Codec origin; + /** + * Ctor. + * @param codec Original codec + */ + public CcBase64(final Codec codec) { + this.origin = codec; + } + + //@todo #19:30min to implement own simple Base64 encode algorithm + // without using 3d-party Base64 encode libraries. Tests for this + // method have been already created, do not forget to remove Ignore + // annotation on it. + @Override + public byte[] encode(final Identity identity) throws IOException { + assert this.origin != null; + throw new UnsupportedOperationException("#encode()"); + } + + //@todo #19:30min to implement own simple Base64 decode algorithm + // without using 3d-party Base64 decode libraries. Tests for this + // method have been already created, do not forget to remove Ignore + // annotation on it. + @Override + public Identity decode(final byte[] bytes) throws IOException { + assert this.origin != null; + throw new UnsupportedOperationException("#decode()"); + } + +} diff --git a/src/main/java/org/takes/facets/auth/social/PsGithub.java b/src/main/java/org/takes/facets/auth/social/PsGithub.java index 0530968cc..9ac012abf 100644 --- a/src/main/java/org/takes/facets/auth/social/PsGithub.java +++ b/src/main/java/org/takes/facets/auth/social/PsGithub.java @@ -147,7 +147,7 @@ private static Identity parse(final JsonObject json) { final ConcurrentMap props = new ConcurrentHashMap(json.size()); // @checkstyle MultipleStringLiteralsCheck (1 line) - props.put("login", json.getString("login", "?")); + props.put("login", json.getString("login", "unknown")); props.put("avatar", json.getString("avatar_url", "#")); return new Identity.Simple( String.format("urn:github:%d", json.getInt("id")), props diff --git a/src/main/java/org/takes/facets/auth/social/PsGoogle.java b/src/main/java/org/takes/facets/auth/social/PsGoogle.java index 34d441539..c89e487d3 100644 --- a/src/main/java/org/takes/facets/auth/social/PsGoogle.java +++ b/src/main/java/org/takes/facets/auth/social/PsGoogle.java @@ -26,11 +26,12 @@ import com.jcabi.http.request.JdkRequest; import com.jcabi.http.response.JsonResponse; import com.jcabi.http.response.RestResponse; -import com.jcabi.http.wire.VerboseWire; import java.io.IOException; import java.net.HttpURLConnection; import java.util.Collections; import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import javax.json.JsonObject; import lombok.EqualsAndHashCode; import org.takes.Request; @@ -50,7 +51,7 @@ * @since 0.9 * @checkstyle MultipleStringLiteralsCheck (500 lines) */ -@EqualsAndHashCode(of = { "app", "key" }) +@EqualsAndHashCode(of = { "app", "key", "redir" }) public final class PsGoogle implements Pass { /** @@ -63,14 +64,22 @@ public final class PsGoogle implements Pass { */ private final transient String key; + /** + * Redirect URI. + */ + private final transient String redir; + /** * Ctor. * @param gapp Google app * @param gkey Google key + * @param uri Redirect URI (exactly as registered in Google console) */ - public PsGoogle(final String gapp, final String gkey) { + public PsGoogle(final String gapp, final String gkey, + final String uri) { this.app = gapp; this.key = gkey; + this.redir = uri; } @Override @@ -84,7 +93,7 @@ public Iterator enter(final Request request) ); } return Collections.singleton( - PsGoogle.fetch(this.token(href, code.next())) + PsGoogle.fetch(this.token(code.next())) ).iterator(); } @@ -115,22 +124,19 @@ private static Identity fetch(final String token) throws IOException { /** * Retrieve Google access token. - * @param home Home of this page * @param code Google "authorization code" * @return The token * @throws IOException If failed */ - private String token(final Href home, final String code) - throws IOException { + private String token(final String code) throws IOException { return new JdkRequest("https://accounts.google.com/o/oauth2/token") .body() .formParam("client_id", this.app) - .formParam("redirect_uri", home) + .formParam("redirect_uri", this.redir) .formParam("client_secret", this.key) .formParam("grant_type", "authorization_code") .formParam("code", code) .back() - .through(VerboseWire.class) .header("Content-Type", "application/x-www-form-urlencoded") .method(com.jcabi.http.Request.POST) .fetch().as(RestResponse.class) @@ -146,9 +152,13 @@ private String token(final Href home, final String code) * @return Identity found */ private static Identity parse(final JsonObject json) { + final ConcurrentMap props = + new ConcurrentHashMap(json.size()); + // @checkstyle MultipleStringLiteralsCheck (1 line) + props.put("picture", json.getString("picture", "#")); + props.put("name", json.getString("name", "unknown")); return new Identity.Simple( - String.format("urn:google:%s", json.getString("id")), - Collections.emptyMap() + String.format("urn:google:%s", json.getString("id")), props ); } diff --git a/src/main/java/org/takes/facets/auth/social/XeGoogleLink.java b/src/main/java/org/takes/facets/auth/social/XeGoogleLink.java index ca82c2747..70f3a9c59 100644 --- a/src/main/java/org/takes/facets/auth/social/XeGoogleLink.java +++ b/src/main/java/org/takes/facets/auth/social/XeGoogleLink.java @@ -51,10 +51,20 @@ public final class XeGoogleLink extends XeWrap { */ public XeGoogleLink(final Request req, final CharSequence app) throws IOException { - this( - req, app, "takes:google", - new RqHref.Smart(new RqHref.Base(req)).home() - ); + this(req, app, new RqHref.Smart(new RqHref.Base(req)).home()); + } + + /** + * Ctor. + * @param req Request + * @param app Google application ID + * @param redir Redirect URI + * @throws IOException If fails + * @since 0.14 + */ + public XeGoogleLink(final Request req, final CharSequence app, + final CharSequence redir) throws IOException { + this(req, app, "takes:google", redir); } /** @@ -64,6 +74,7 @@ public XeGoogleLink(final Request req, final CharSequence app) * @param rel Related * @param redir Redirect URI * @throws IOException If fails + * @since 0.14 * @checkstyle ParameterNumberCheck (4 lines) */ public XeGoogleLink(final Request req, final CharSequence app, diff --git a/src/main/java/org/takes/facets/fork/FkMethods.java b/src/main/java/org/takes/facets/fork/FkMethods.java index b7a3a7129..2b16321fa 100644 --- a/src/main/java/org/takes/facets/fork/FkMethods.java +++ b/src/main/java/org/takes/facets/fork/FkMethods.java @@ -87,7 +87,7 @@ public FkMethods(final Collection mtds, final Take tke) { @Override public Iterator route(final Request req) throws IOException { - final String mtd = new RqMethod(req).method(); + final String mtd = new RqMethod.Base(req).method(); final Collection list = new ArrayList(1); if (this.methods.contains(mtd)) { list.add(this.take.act(req)); diff --git a/src/main/java/org/takes/facets/fork/TkProduces.java b/src/main/java/org/takes/facets/fork/TkProduces.java new file mode 100644 index 000000000..be9cb1f51 --- /dev/null +++ b/src/main/java/org/takes/facets/fork/TkProduces.java @@ -0,0 +1,64 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015 Yegor Bugayenko + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.takes.facets.fork; + +import java.io.IOException; +import lombok.EqualsAndHashCode; +import org.takes.Request; +import org.takes.Response; +import org.takes.Take; +import org.takes.tk.TkWrap; + +/** + * Take that acts on request with specified "Accept" HTTP headers only. + * + *

The class is immutable and thread-safe. + * + * @author Eugene Kondrashev (eugene.kondrashev@gmail.com) + * @version $Id$ + * @since 0.14 + */ +@EqualsAndHashCode(callSuper = true) +public final class TkProduces extends TkWrap { + + /** + * Ctor. + * @param take Original take + * @param types Accept types + */ + public TkProduces(final Take take, final String types) { + super( + new Take() { + @Override + public Response act(final Request req) throws IOException { + return new RsFork( + req, + new FkTypes(types, take.act(req)) + ); + } + } + ); + } + +} diff --git a/src/main/java/org/takes/facets/forward/RsForward.java b/src/main/java/org/takes/facets/forward/RsForward.java index 6f28b02df..20879a572 100644 --- a/src/main/java/org/takes/facets/forward/RsForward.java +++ b/src/main/java/org/takes/facets/forward/RsForward.java @@ -33,6 +33,7 @@ import org.takes.rs.RsEmpty; import org.takes.rs.RsWithHeader; import org.takes.rs.RsWithStatus; +import org.takes.rs.RsWithoutHeader; /** * Forwarding response. @@ -71,6 +72,15 @@ public RsForward(final Response res) { this(res, "/"); } + /** + * Ctor. + * @param res Original response + * @since 0.14 + */ + public RsForward(final RsForward res) { + this(res.origin); + } + /** * Ctor. * @param res Original response @@ -80,6 +90,16 @@ public RsForward(final Response res, final CharSequence loc) { this(res, HttpURLConnection.HTTP_SEE_OTHER, loc); } + /** + * Ctor. + * @param res Original response + * @param loc Location + * @since 0.14 + */ + public RsForward(final RsForward res, final CharSequence loc) { + this(res.origin, loc); + } + /** * Ctor. * @param loc Location @@ -97,6 +117,18 @@ public RsForward(final int code, final CharSequence loc) { this(new RsEmpty(), code, loc); } + /** + * Ctor. + * @param res Original + * @param code HTTP status code + * @param loc Location + * @since 0.14 + */ + public RsForward(final RsForward res, final int code, + final CharSequence loc) { + this(res.origin, code, loc); + } + /** * Ctor. * @param res Original @@ -107,7 +139,10 @@ public RsForward(final Response res, final int code, final CharSequence loc) { super(code, res.toString()); this.origin = new RsWithHeader( - new RsWithStatus(res, code), + new RsWithoutHeader( + new RsWithStatus(res, code), + "Location" + ), new Sprintf("Location: %s", loc) ); } diff --git a/src/main/java/org/takes/facets/forward/package-info.java b/src/main/java/org/takes/facets/forward/package-info.java index 0cb4d71b8..23f979499 100644 --- a/src/main/java/org/takes/facets/forward/package-info.java +++ b/src/main/java/org/takes/facets/forward/package-info.java @@ -37,7 +37,7 @@ * @Override * public Response act(final Request req) { * final InputStream content = - * new RqMultipart(req).part("file").body(); + * new RqMultipart.Base(req).part("file").body(); * // save content to whenever you want * return new RsForward(new RqHref(req).href()); * } diff --git a/src/main/java/org/takes/misc/Href.java b/src/main/java/org/takes/misc/Href.java index 7e9acf5b3..2e265c739 100644 --- a/src/main/java/org/takes/misc/Href.java +++ b/src/main/java/org/takes/misc/Href.java @@ -123,10 +123,7 @@ public CharSequence subSequence(final int start, final int end) { @Override public String toString() { - final StringBuilder text = new StringBuilder(this.uri.toString()); - if (this.uri.getPath().isEmpty()) { - text.append('/'); - } + final StringBuilder text = new StringBuilder(this.bare()); if (!this.params.isEmpty()) { boolean first = true; for (final Map.Entry> ent @@ -157,6 +154,19 @@ public String path() { return this.uri.getPath(); } + /** + * Get URI without params. + * @return Bare URI + * @since 0.14 + */ + public String bare() { + final StringBuilder text = new StringBuilder(this.uri.toString()); + if (this.uri.getPath().isEmpty()) { + text.append('/'); + } + return text.toString(); + } + /** * Get query param. * @param key Param name diff --git a/src/main/java/org/takes/misc/Transformer.java b/src/main/java/org/takes/misc/Transformer.java index 8f4ad5472..a24922ecb 100644 --- a/src/main/java/org/takes/misc/Transformer.java +++ b/src/main/java/org/takes/misc/Transformer.java @@ -28,18 +28,18 @@ import java.util.List; /** - * Transform elements in an iterable into others. + * Transform elements in an iterable (in type T) into others (in type K). * * @author Jason Wong (super132j@yahoo.com) * @version $Id$ * @since 0.13.8 */ -public class Transformer implements Iterable { +public class Transformer implements Iterable { /** * Internal storage. */ - private final transient List storage = new LinkedList(); + private final transient List storage = new LinkedList(); /** * Transform elements in the supplied iterable by the action supplied. @@ -48,7 +48,7 @@ public class Transformer implements Iterable { * @param action The actual transformation implementation */ public Transformer(final Iterable list, - final Transformer.Action action) { + final Transformer.Action action) { final Iterator itr = list.iterator(); while (itr.hasNext()) { this.storage.add(action.transform(itr.next())); @@ -56,18 +56,18 @@ public Transformer(final Iterable list, } @Override - public final Iterator iterator() { + public final Iterator iterator() { return this.storage.iterator(); } - public interface Action { + public interface Action { /** - * The transform action of the element. + * The transform action of the element of type T to K. * * @param element Element of the iterable * @return Transformed element */ - T transform(T element); + K transform(T element); } /** @@ -79,12 +79,29 @@ public interface Action { * */ public static final class Trim implements - Transformer.Action { + Transformer.Action { @Override public String transform(final String element) { return element.trim(); } + } + + /** + * Convert CharSequence into String + * + * @author Jason Wong (super132j@yahoo.com) + * @version $Id$ + * @since 0.13.8 + * + */ + public static final class ToString implements + Transformer.Action { -} + @Override + public String transform(CharSequence element) { + return element.toString(); + } + + } } diff --git a/src/main/java/org/takes/rq/RqForm.java b/src/main/java/org/takes/rq/RqForm.java index c3ee25c8c..b4ae011e3 100644 --- a/src/main/java/org/takes/rq/RqForm.java +++ b/src/main/java/org/takes/rq/RqForm.java @@ -25,16 +25,20 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; import java.net.URLDecoder; import java.nio.charset.Charset; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import lombok.EqualsAndHashCode; +import org.takes.HttpException; import org.takes.Request; import org.takes.misc.Sprintf; import org.takes.misc.VerboseIterable; @@ -44,7 +48,7 @@ * {@code application/x-www-form-urlencoded} format (RFC 1738). * *

For {@code multipart/form-data} format use - * {@link org.takes.rq.RqMultipart}. + * {@link org.takes.rq.RqMultipart.Base}. * *

It is highly recommended to use {@link org.takes.rq.RqGreedy} * decorator before passing request to this class. @@ -57,94 +61,177 @@ * @link Forms in HTML * @see org.takes.rq.RqGreedy */ -@EqualsAndHashCode(callSuper = true, of = "map") -public final class RqForm extends RqWrap { +@SuppressWarnings("PMD.TooManyMethods") +public interface RqForm extends Request { /** - * Map of params and values. + * Get single parameter. + * @param name Parameter name + * @return List of values (can be empty) */ - private final transient ConcurrentMap> map; + Iterable param(CharSequence name); /** - * Ctor. - * @param req Original request - * @throws IOException If fails + * Get all parameter names. + * @return All names */ - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public RqForm(final Request req) throws IOException { - super(req); - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - new RqPrint(req).printBody(baos); - final String body = new String(baos.toByteArray()); - this.map = new ConcurrentHashMap>(0); - for (final String pair : body.split("&")) { - if (pair.isEmpty()) { - continue; + Iterable names(); + + /** + * Base implementation of @link RqForm. + * @author Aleksey Popov (alopen@yandex.ru) + * @version $Id$ + */ + @EqualsAndHashCode(callSuper = true, of = "map") + final class Base extends RqWrap implements RqForm { + /** + * Map of params and values. + */ + private final transient ConcurrentMap> map; + /** + * Ctor. + * @param req Original request + * @throws IOException If fails + */ + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + public Base(final Request req) throws IOException { + super(req); + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new RqPrint(req).printBody(baos); + final String body = new String(baos.toByteArray()); + this.map = new ConcurrentHashMap>(0); + for (final String pair : body.split("&")) { + if (pair.isEmpty()) { + continue; + } + final String[] parts = pair.split("=", 2); + if (parts.length < 2) { + throw new IOException( + String.format("invalid form body pair: %s", pair) + ); + } + final String key = RqForm.Base.decode( + parts[0].trim().toLowerCase(Locale.ENGLISH) + ); + this.map.putIfAbsent(key, new LinkedList()); + this.map.get(key).add(RqForm.Base.decode(parts[1].trim())); } - final String[] parts = pair.split("=", 2); - if (parts.length < 2) { - throw new IOException( - String.format("invalid form body pair: %s", pair) + } + @Override + public Iterable param(final CharSequence key) { + final List values = + this.map.get(key.toString().toLowerCase(Locale.ENGLISH)); + final Iterable iter; + if (values == null) { + iter = new VerboseIterable( + Collections.emptyList(), + new Sprintf( + "there're no params by name \"%s\" among %d others: %s", + key, this.map.size(), this.map.keySet() + ) + ); + } else { + iter = new VerboseIterable( + values, + new Sprintf( + "there are only %d params by name \"%s\"", + values.size(), key + ) ); } - final String key = RqForm.decode( - parts[0].trim().toLowerCase(Locale.ENGLISH) - ); - this.map.putIfAbsent(key, new LinkedList()); - this.map.get(key).add(RqForm.decode(parts[1].trim())); + return iter; } - } - - /** - * Get single parameter. - * @param key Parameter name - * @return List of values (can be empty) - */ - public Iterable param(final CharSequence key) { - final List values = - this.map.get(key.toString().toLowerCase(Locale.ENGLISH)); - final Iterable iter; - if (values == null) { - iter = new VerboseIterable( - Collections.emptyList(), - new Sprintf( - "there are no params by name \"%s\" among %d others: %s", - key, this.map.size(), this.map.keySet() - ) - ); - } else { - iter = new VerboseIterable( - values, - new Sprintf( - "there are only %d params by name \"%s\"", - values.size(), key - ) - ); + @Override + public Iterable names() { + return this.map.keySet(); + } + /** + * Decode from URL. + * @param txt Text + * @return Decoded + */ + private static String decode(final CharSequence txt) { + try { + return URLDecoder.decode( + txt.toString(), Charset.defaultCharset().name() + ); + } catch (final UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); + } } - return iter; - } - - /** - * Get all parameter names. - * @return All names - */ - public Iterable names() { - return this.map.keySet(); } - /** - * Decode from URL. - * @param txt Text - * @return Decoded + * Smart decorator, with extra features. + * + *

The class is immutable and thread-safe. + * + * @author Yegor Bugayenko (yegor@teamed.io) + * @since 0.14 */ - private static String decode(final CharSequence txt) { - try { - return URLDecoder.decode( - txt.toString(), Charset.defaultCharset().name() - ); - } catch (final UnsupportedEncodingException ex) { - throw new IllegalStateException(ex); + @EqualsAndHashCode(of = "origin") + final class Smart implements RqForm { + /** + * Original. + */ + private final transient RqForm origin; + /** + * Ctor. + * @param req Original request + */ + public Smart(final RqForm req) { + this.origin = req; + } + @Override + public Iterable param(final CharSequence name) { + return this.origin.param(name); + } + @Override + public Iterable names() { + return this.origin.names(); + } + @Override + public Iterable head() throws IOException { + return this.origin.head(); + } + @Override + public InputStream body() throws IOException { + return this.origin.body(); + } + /** + * Get single param or throw HTTP exception. + * @param name Name of query param + * @return Value of it + * @throws IOException If fails + */ + public String single(final CharSequence name) throws IOException { + final Iterator params = this.origin + .param(name).iterator(); + if (!params.hasNext()) { + throw new HttpException( + HttpURLConnection.HTTP_BAD_REQUEST, + String.format( + "form param \"%s\" is mandatory", name + ) + ); + } + return params.next(); + } + /** + * Get single param or default. + * @param name Name of query param + * @param def Default, if not found + * @return Value of it + */ + public String single(final CharSequence name, final String def) { + final String value; + final Iterator params = this.origin + .param(name).iterator(); + if (params.hasNext()) { + value = params.next(); + } else { + value = def; + } + return value; } } - } diff --git a/src/main/java/org/takes/rq/RqHref.java b/src/main/java/org/takes/rq/RqHref.java index 44c943edc..afacc51fb 100644 --- a/src/main/java/org/takes/rq/RqHref.java +++ b/src/main/java/org/takes/rq/RqHref.java @@ -137,7 +137,7 @@ public Href home() throws IOException { * @return Value of it * @throws IOException If fails */ - public String param(final String name) throws IOException { + public String single(final CharSequence name) throws IOException { final Iterator params = this.origin.href() .param(name).iterator(); if (!params.hasNext()) { @@ -157,7 +157,7 @@ public String param(final String name) throws IOException { * @return Value of it * @throws IOException If fails */ - public String param(final String name, final String def) + public String single(final CharSequence name, final CharSequence def) throws IOException { final String value; final Iterator params = this.origin.href() @@ -165,7 +165,7 @@ public String param(final String name, final String def) if (params.hasNext()) { value = params.next(); } else { - value = def; + value = def.toString(); } return value; } diff --git a/src/main/java/org/takes/rq/RqMethod.java b/src/main/java/org/takes/rq/RqMethod.java index 9154d6e1b..125653ca9 100644 --- a/src/main/java/org/takes/rq/RqMethod.java +++ b/src/main/java/org/takes/rq/RqMethod.java @@ -29,69 +29,83 @@ import org.takes.Request; /** - * Request decorator, for HTTP method parsing. + * HTTP method parsing. * - *

The class is immutable and thread-safe. + *

All implementations of this interface must be immutable and thread-safe. * * @author Yegor Bugayenko (yegor@teamed.io) * @version $Id$ - * @since 0.1 + * @since 0.13.7 */ -@EqualsAndHashCode(callSuper = true) -public final class RqMethod extends RqWrap { +public interface RqMethod extends Request { /** * GET method. */ - public static final String GET = "GET"; + String GET = "GET"; /** * POST method. */ - public static final String POST = "POST"; + String POST = "POST"; /** * PUT method. */ - public static final String PUT = "PUT"; + String PUT = "PUT"; /** * DELETE method. */ - public static final String DELETE = "DELETE"; + String DELETE = "DELETE"; /** * HEAD method. */ - public static final String HEAD = "HEAD"; + String HEAD = "HEAD"; /** * OPTIONS method. */ - public static final String OPTIONS = "OPTIONS"; + String OPTIONS = "OPTIONS"; /** * PATCH method. */ - public static final String PATCH = "PATCH"; - - /** - * Ctor. - * @param req Original request - */ - public RqMethod(final Request req) { - super(req); - } + String PATCH = "PATCH"; /** * Get method. * @return HTTP method * @throws IOException If fails */ - public String method() throws IOException { - final String line = this.head().iterator().next(); - final String[] parts = line.split(" ", 2); - return parts[0].toUpperCase(Locale.ENGLISH); - } + String method() throws IOException; + /** + * Request decorator, for HTTP method parsing. + * + *

The class is immutable and thread-safe. + * + * @author Dmitry Zaytsev (dmitry.zaytsev@gmail.com) + * @version $Id$ + * @since 0.13.7 + */ + @EqualsAndHashCode(callSuper = true) + final class Base extends RqWrap implements RqMethod { + + /** + * Ctor. + * @param req Original request + */ + public Base(final Request req) { + super(req); + } + + @Override + public String method() throws IOException { + final String line = this.head().iterator().next(); + final String[] parts = line.split(" ", 2); + return parts[0].toUpperCase(Locale.ENGLISH); + } + } } diff --git a/src/main/java/org/takes/rq/RqMultipart.java b/src/main/java/org/takes/rq/RqMultipart.java index 3753295a5..59cbe6bc9 100644 --- a/src/main/java/org/takes/rq/RqMultipart.java +++ b/src/main/java/org/takes/rq/RqMultipart.java @@ -42,200 +42,220 @@ import org.takes.misc.VerboseIterable; /** - * Request decorator that decodes FORM data from - * {@code multipart/form-data} format (RFC 2045). + * HTTP multipart FORM data decoding. * - *

For {@code application/x-www-form-urlencoded} - * format use {@link org.takes.rq.RqForm}. - * - *

It is highly recommended to use {@link org.takes.rq.RqGreedy} - * decorator before passing request to this class. - * - *

The class is immutable and thread-safe. + *

All implementations of this interface must be immutable and thread-safe. * * @author Yegor Bugayenko (yegor@teamed.io) * @version $Id$ * @since 0.9 - * @link Forms in HTML - * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) - * @see org.takes.rq.RqGreedy */ -@EqualsAndHashCode(callSuper = true) -public final class RqMultipart extends RqWrap { - - /** - * Pattern to get boundary from header. - */ - private static final Pattern BOUNDARY = Pattern.compile( - ".*[^a-z]boundary=([^;]+).*" - ); - - /** - * Pattern to get name from header. - */ - private static final Pattern NAME = Pattern.compile( - ".*[^a-z]name=\"([^\"]+)\".*" - ); - - /** - * Map of params and values. - */ - private final transient ConcurrentMap> map; - - /** - * Ctor. - * @param req Original request - * @throws IOException If fails - */ - public RqMultipart(final Request req) throws IOException { - super(req); - final String header = new RqHeaders(req).header("Content-Type") - .iterator().next(); - if (!header.toLowerCase(Locale.ENGLISH) - .startsWith("multipart/form-data")) { - throw new IOException( - String.format( - // @checkstyle LineLength (1 line) - "RqMultipart can only parse multipart/form-data, while Content-Type specifies a different type: %s", - header - ) - ); - } - final Matcher matcher = RqMultipart.BOUNDARY.matcher(header); - if (!matcher.matches()) { - throw new IOException( - String.format( - "boundary is not specified in Content-Type header: %s", - header - ) - ); - } - final Collection requests = new LinkedList(); - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - new RqPrint(req).printBody(baos); - final byte[] boundary = matcher.group(1).getBytes(); - final byte[] body = baos.toByteArray(); - int pos = 0; - while (pos < body.length) { - int start = pos + boundary.length + 2; - if (body[start] == '-') { - break; - } - start += 2; - final int stop = RqMultipart.indexOf(body, boundary, start) - 2; - requests.add(this.make(body, start, stop - 2)); - pos = stop; - } - this.map = RqMultipart.asMap(requests); - } - +public interface RqMultipart extends Request { /** * Get single part. * @param name Name of the part to get * @return List of parts (can be empty) */ - public Iterable part(final CharSequence name) { - final List values = this.map - .get(name.toString().toLowerCase(Locale.ENGLISH)); - final Iterable iter; - if (values == null) { - iter = new VerboseIterable( - Collections.emptyList(), - new Sprintf( - "there are no parts by name \"%s\" among %d others: %s", - name, this.map.size(), this.map.keySet() - ) - ); - } else { - iter = new VerboseIterable( - values, - new Sprintf( - "there are just %d parts by name \"%s\"", - values.size(), name - ) - ); - } - return iter; - } + Iterable part(final CharSequence name); /** * Get all part names. * @return All names */ - public Iterable names() { - return this.map.keySet(); - } + Iterable names(); /** - * Make a request. - * @param body Body - * @param start Start position - * @param stop Stop position - * @return Request - * @throws IOException If fails + * Request decorator, that decodes FORM data from + * {@code multipart/form-data} format (RFC 2045). + * + *

For {@code application/x-www-form-urlencoded} + * format use {@link org.takes.rq.RqForm}. + * + *

It is highly recommended to use {@link org.takes.rq.RqGreedy} + * decorator before passing request to this class. + * + *

The class is immutable and thread-safe. + * + * @author Yegor Bugayenko (yegor@teamed.io) + * @version $Id$ + * @since 0.9 + * @link Forms in HTML + * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) + * @see org.takes.rq.RqGreedy */ - private Request make(final byte[] body, final int start, - final int stop) throws IOException { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - baos.write(this.head().iterator().next().getBytes()); - baos.write("\r\n".getBytes()); - baos.write(Arrays.copyOfRange(body, start, stop)); - return new RqLive(new ByteArrayInputStream(baos.toByteArray())); - } + @EqualsAndHashCode(callSuper = true) + final class Base extends RqWrap implements RqMultipart { + /** + * Pattern to get boundary from header. + */ + private static final Pattern BOUNDARY = Pattern.compile( + ".*[^a-z]boundary=([^;]+).*" + ); - /** - * Convert a list of requests to a map. - * @param reqs Requests - * @return Map of them - * @throws IOException If fails - */ - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - private static ConcurrentMap> asMap( - final Collection reqs) throws IOException { - final ConcurrentMap> map = - new ConcurrentHashMap>(reqs.size()); - for (final Request req : reqs) { - final String header = new RqHeaders(req) - .header("Content-Disposition").iterator().next(); - final Matcher matcher = RqMultipart.NAME.matcher(header); + /** + * Pattern to get name from header. + */ + private static final Pattern NAME = Pattern.compile( + ".*[^a-z]name=\"([^\"]+)\".*" + ); + + /** + * Map of params and values. + */ + private final transient ConcurrentMap> map; + + /** + * Ctor. + * @param req Original request + * @throws IOException If fails + */ + public Base(final Request req) throws IOException { + super(req); + final String header = new RqHeaders(req).header("Content-Type") + .iterator().next(); + if (!header.toLowerCase(Locale.ENGLISH) + .startsWith("multipart/form-data")) { + throw new IOException( + String.format( + // @checkstyle LineLength (1 line) + "RqMultipart.Base can only parse multipart/form-data, while Content-Type specifies a different type: %s", + header + ) + ); + } + final Matcher matcher = Base.BOUNDARY.matcher(header); if (!matcher.matches()) { throw new IOException( String.format( - "\"name\" not found in Content-Disposition header: %s", + "boundary is not specified in Content-Type header: %s", header ) ); } - final String name = matcher.group(1); - map.putIfAbsent(name, new LinkedList()); - map.get(name).add(req); + final Collection requests = new LinkedList(); + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new RqPrint(req).printBody(baos); + final byte[] boundary = matcher.group(1).getBytes(); + final byte[] body = baos.toByteArray(); + int pos = 0; + while (pos < body.length) { + int start = pos + boundary.length + 2; + if (body[start] == '-') { + break; + } + start += 2; + final int stop = RqMultipart.Base.indexOf( + body, + boundary, + start + ) - 2; + requests.add(this.make(body, start, stop - 2)); + pos = stop; + } + this.map = RqMultipart.Base.asMap(requests); } - return map; - } - /** - * Find position of array inside another array. - * @param outer Big array - * @param inner Small array - * @param start Where to start searching - * @return Position - * @throws IOException If fails - */ - private static int indexOf(final byte[] outer, final byte[] inner, - final int start) throws IOException { - for (int idx = start; idx < outer.length - inner.length; ++idx) { - boolean found = true; - for (int sub = 0; sub < inner.length; ++sub) { - if (outer[idx + sub] != inner[sub]) { - found = false; - break; + @Override + public Iterable part(final CharSequence name) { + final List values = this.map + .get(name.toString().toLowerCase(Locale.ENGLISH)); + final Iterable iter; + if (values == null) { + iter = new VerboseIterable( + Collections.emptyList(), + new Sprintf( + "there are no parts by name \"%s\" among %d others: %s", + name, this.map.size(), this.map.keySet() + ) + ); + } else { + iter = new VerboseIterable( + values, + new Sprintf( + "there are just %d parts by name \"%s\"", + values.size(), name + ) + ); + } + return iter; + } + + @Override + public Iterable names() { + return this.map.keySet(); + } + + /** + * Make a request. + * @param body Body + * @param start Start position + * @param stop Stop position + * @return Request + * @throws IOException If fails + */ + private Request make(final byte[] body, final int start, + final int stop) throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(this.head().iterator().next().getBytes()); + baos.write("\r\n".getBytes()); + baos.write(Arrays.copyOfRange(body, start, stop)); + return new RqLive(new ByteArrayInputStream(baos.toByteArray())); + } + + /** + * Convert a list of requests to a map. + * @param reqs Requests + * @return Map of them + * @throws IOException If fails + */ + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + private static ConcurrentMap> asMap( + final Collection reqs) throws IOException { + final ConcurrentMap> map = + new ConcurrentHashMap>(reqs.size()); + for (final Request req : reqs) { + final String header = new RqHeaders(req) + .header("Content-Disposition").iterator().next(); + final Matcher matcher = Base.NAME.matcher(header); + if (!matcher.matches()) { + throw new IOException( + String.format( + // @checkstyle LineLength (1 line) + "\"name\" not found in Content-Disposition header: %s", + header + ) + ); } + final String name = matcher.group(1); + map.putIfAbsent(name, new LinkedList()); + map.get(name).add(req); } - if (found) { - return idx; + return map; + } + + /** + * Find position of array inside another array. + * @param outer Big array + * @param inner Small array + * @param start Where to start searching + * @return Position + * @throws IOException If fails + */ + private static int indexOf(final byte[] outer, final byte[] inner, + final int start) throws IOException { + for (int idx = start; idx < outer.length - inner.length; ++idx) { + boolean found = true; + for (int sub = 0; sub < inner.length; ++sub) { + if (outer[idx + sub] != inner[sub]) { + found = false; + break; + } + } + if (found) { + return idx; + } } + throw new IOException("closing boundary not found"); } - throw new IOException("closing boundary not found"); } - } diff --git a/src/main/java/org/takes/rs/RsFluent.java b/src/main/java/org/takes/rs/RsFluent.java index bc45c9507..9f949b25f 100644 --- a/src/main/java/org/takes/rs/RsFluent.java +++ b/src/main/java/org/takes/rs/RsFluent.java @@ -68,7 +68,7 @@ public RsFluent withStatus(final int code) { * @param header The header * @return New fluent response */ - public RsFluent withHeader(final String header) { + public RsFluent withHeader(final CharSequence header) { return new RsFluent(new RsWithHeader(this, header)); } @@ -78,7 +78,8 @@ public RsFluent withHeader(final String header) { * @param value Value * @return New fluent response */ - public RsFluent withHeader(final String key, final String value) { + public RsFluent withHeader(final CharSequence key, + final CharSequence value) { return new RsFluent(new RsWithHeader(this, key, value)); } @@ -87,7 +88,7 @@ public RsFluent withHeader(final String key, final String value) { * @param ctype Content type * @return New fluent response */ - public RsFluent withType(final String ctype) { + public RsFluent withType(final CharSequence ctype) { return new RsFluent(new RsWithType(this, ctype)); } @@ -96,7 +97,7 @@ public RsFluent withType(final String ctype) { * @param body Body * @return New fluent response */ - public RsFluent withBody(final String body) { + public RsFluent withBody(final CharSequence body) { return new RsFluent(new RsWithBody(this, body)); } diff --git a/src/main/java/org/takes/rs/RsHTML.java b/src/main/java/org/takes/rs/RsHTML.java index 90bc39a16..be9f07f2b 100644 --- a/src/main/java/org/takes/rs/RsHTML.java +++ b/src/main/java/org/takes/rs/RsHTML.java @@ -53,7 +53,7 @@ public RsHTML() { * Ctor. * @param body HTML body */ - public RsHTML(final String body) { + public RsHTML(final CharSequence body) { this(new RsEmpty(), body); } @@ -87,7 +87,7 @@ public RsHTML(final InputStream body) { * @param res Original response * @param body HTML body */ - public RsHTML(final Response res, final String body) { + public RsHTML(final Response res, final CharSequence body) { this(new RsWithBody(res, body)); } diff --git a/src/main/java/org/takes/rs/RsRedirect.java b/src/main/java/org/takes/rs/RsRedirect.java index c1c8900a5..cfe6c6115 100644 --- a/src/main/java/org/takes/rs/RsRedirect.java +++ b/src/main/java/org/takes/rs/RsRedirect.java @@ -49,7 +49,7 @@ public RsRedirect() { * Ctor. * @param location Where to redirect */ - public RsRedirect(final String location) { + public RsRedirect(final CharSequence location) { this(location, HttpURLConnection.HTTP_SEE_OTHER); } @@ -58,7 +58,7 @@ public RsRedirect(final String location) { * @param location Location * @param code HTTP redirect status code */ - public RsRedirect(final String location, final int code) { + public RsRedirect(final CharSequence location, final int code) { super( new RsWithHeader( new RsWithStatus(new RsEmpty(), code), diff --git a/src/main/java/org/takes/rs/RsText.java b/src/main/java/org/takes/rs/RsText.java index 111c6ac68..385bab64a 100644 --- a/src/main/java/org/takes/rs/RsText.java +++ b/src/main/java/org/takes/rs/RsText.java @@ -53,7 +53,7 @@ public RsText() { * Ctor. * @param body Plain text body */ - public RsText(final String body) { + public RsText(final CharSequence body) { this(new RsEmpty(), body); } @@ -87,7 +87,7 @@ public RsText(final URL url) { * @param res Original response * @param body HTML body */ - public RsText(final Response res, final String body) { + public RsText(final Response res, final CharSequence body) { this(new RsWithBody(res, body)); } diff --git a/src/main/java/org/takes/rs/RsVelocity.java b/src/main/java/org/takes/rs/RsVelocity.java index 4216fa219..40420c78d 100644 --- a/src/main/java/org/takes/rs/RsVelocity.java +++ b/src/main/java/org/takes/rs/RsVelocity.java @@ -76,9 +76,9 @@ public final class RsVelocity extends RsWrap { * @param params List of params * @since 0.11 */ - public RsVelocity(final String template, + public RsVelocity(final CharSequence template, final RsVelocity.Pair... params) { - this(new ByteArrayInputStream(template.getBytes()), params); + this(new ByteArrayInputStream(template.toString().getBytes()), params); } /** @@ -108,7 +108,8 @@ public RsVelocity(final InputStream template, * @param tpl Template * @param params Map of params */ - public RsVelocity(final InputStream tpl, final Map params) { + public RsVelocity(final InputStream tpl, final Map params) { super( new Response() { @Override @@ -131,7 +132,7 @@ public InputStream body() throws IOException { * @throws IOException If fails */ private static InputStream render(final InputStream page, - final Map params) throws IOException { + final Map params) throws IOException { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final Writer writer = new OutputStreamWriter(baos); final VelocityEngine engine = new VelocityEngine(); @@ -154,11 +155,11 @@ private static InputStream render(final InputStream page, * @param entries Entries * @return Map */ - private static Map asMap( - final Map.Entry... entries) { - final ConcurrentMap map = - new ConcurrentHashMap(entries.length); - for (final Map.Entry ent : entries) { + private static Map asMap( + final Map.Entry... entries) { + final ConcurrentMap map = + new ConcurrentHashMap(entries.length); + for (final Map.Entry ent : entries) { map.put(ent.getKey(), ent.getValue()); } return map; @@ -168,7 +169,7 @@ private static Map asMap( * Pair of values. */ public static final class Pair - extends AbstractMap.SimpleEntry { + extends AbstractMap.SimpleEntry { /** * Serialization marker. */ @@ -178,7 +179,7 @@ public static final class Pair * @param key Key * @param obj Pass */ - public Pair(final String key, final Object obj) { + public Pair(final CharSequence key, final Object obj) { super(key, obj); } } diff --git a/src/main/java/org/takes/rs/RsWithBody.java b/src/main/java/org/takes/rs/RsWithBody.java index 846ed6fa8..5180b6742 100644 --- a/src/main/java/org/takes/rs/RsWithBody.java +++ b/src/main/java/org/takes/rs/RsWithBody.java @@ -46,7 +46,7 @@ public final class RsWithBody extends RsWrap { * Ctor. * @param body Body */ - public RsWithBody(final String body) { + public RsWithBody(final CharSequence body) { this(new RsEmpty(), body); } @@ -79,8 +79,8 @@ public RsWithBody(final URL url) { * @param res Original response * @param body Body */ - public RsWithBody(final Response res, final String body) { - this(res, body.getBytes()); + public RsWithBody(final Response res, final CharSequence body) { + this(res, body.toString().getBytes()); } /** diff --git a/src/main/java/org/takes/rs/RsWithCookie.java b/src/main/java/org/takes/rs/RsWithCookie.java index 7bf7b132d..3f68288b4 100644 --- a/src/main/java/org/takes/rs/RsWithCookie.java +++ b/src/main/java/org/takes/rs/RsWithCookie.java @@ -23,12 +23,18 @@ */ package org.takes.rs; +import java.util.regex.Pattern; import lombok.EqualsAndHashCode; import org.takes.Response; /** * Response decorator, with an additional cookie. * + * The decorator validates cookie name according + * @see RFC 2616 + * and cookie value according + * @see RFC 6265 + * *

Use this decorator in order to return a response with a "Set-Cookie" * header inside, for example: * @@ -51,14 +57,29 @@ @EqualsAndHashCode(callSuper = true) public final class RsWithCookie extends RsWrap { + /** + * Cookie value validation regexp. + * @checkstyle LineLengthCheck (3 lines) + */ + private static final Pattern CVALUE_PTRN = Pattern.compile( + "[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*|\"[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*\"" + ); + + /** + * Cookie name validation regexp. + */ + private static final Pattern CNAME_PTRN = Pattern.compile( + "[\\x20-\\x7E&&[^()<>@,;:\\\"/\\[\\]?={} ]]+" + ); + /** * Ctor. * @param name Cookie name * @param value Value of it * @param attrs Optional attributes, for example "Path=/" */ - public RsWithCookie(final String name, final String value, - final String... attrs) { + public RsWithCookie(final CharSequence name, final CharSequence value, + final CharSequence... attrs) { this(new RsEmpty(), name, value, attrs); } @@ -70,11 +91,17 @@ public RsWithCookie(final String name, final String value, * @param attrs Optional attributes, for example "Path=/" * @checkstyle ParameterNumberCheck (5 lines) */ - public RsWithCookie(final Response res, final String name, - final String value, final String... attrs) { + public RsWithCookie(final Response res, final CharSequence name, + final CharSequence value, final CharSequence... attrs) { super( new RsWithHeader( - res, "Set-Cookie", RsWithCookie.make(name, value, attrs) + res, + "Set-Cookie", + RsWithCookie.make( + RsWithCookie.checkName(name), + RsWithCookie.checkValue(value), + attrs + ) ) ); } @@ -86,15 +113,48 @@ public RsWithCookie(final Response res, final String name, * @param attrs Optional attributes, for example "Path=/" * @return Text */ - private static String make(final String name, - final String value, final String... attrs) { + private static String make(final CharSequence name, + final CharSequence value, final CharSequence... attrs) { final StringBuilder text = new StringBuilder( String.format("%s=%s", name, value) ); - for (final String attr : attrs) { + for (final CharSequence attr : attrs) { text.append(';').append(attr); } return text.toString(); } + /** + * Checks value according RFC 6265 section 4.1.1. + * @param value Cookie value + * @return Cookie value + */ + private static CharSequence checkValue(final CharSequence value) { + if (!RsWithCookie.CVALUE_PTRN.matcher(value).matches()) { + throw new IllegalArgumentException( + String.format( + "Cookie value %s contains invalid characters", + value + ) + ); + } + return value; + } + + /** + * Checks name according RFC 2616, section 2.2. + * @param name Cookie name; + * @return Cookie name + */ + private static CharSequence checkName(final CharSequence name) { + if (!RsWithCookie.CNAME_PTRN.matcher(name).matches()) { + throw new IllegalArgumentException( + String.format( + "Cookie name %s contains invalid characters", + name + ) + ); + } + return name; + } } diff --git a/src/main/java/org/takes/rs/RsWithHeader.java b/src/main/java/org/takes/rs/RsWithHeader.java index 20dbc2ce7..271ad3755 100644 --- a/src/main/java/org/takes/rs/RsWithHeader.java +++ b/src/main/java/org/takes/rs/RsWithHeader.java @@ -123,12 +123,12 @@ public InputStream body() throws IOException { */ private static Iterable extend(final Iterable head, final String header) throws IOException { - if (!HEADER.matcher(header).matches()) { + if (!RsWithHeader.HEADER.matcher(header).matches()) { throw new IllegalArgumentException( String.format( // @checkstyle LineLength (1 line) "header line of HTTP response \"%s\" doesn't match \"%s\" regular expression, but it should, according to RFC 7230", - header, HEADER + header, RsWithHeader.HEADER ) ); } diff --git a/src/main/java/org/takes/rs/RsWithHeaders.java b/src/main/java/org/takes/rs/RsWithHeaders.java index 372cf6959..784e62f81 100644 --- a/src/main/java/org/takes/rs/RsWithHeaders.java +++ b/src/main/java/org/takes/rs/RsWithHeaders.java @@ -26,10 +26,13 @@ import java.io.IOException; import java.io.InputStream; import java.util.Arrays; + import lombok.EqualsAndHashCode; + import org.takes.Response; import org.takes.misc.Concat; import org.takes.misc.Transformer; +import org.takes.misc.Transformer.Trim; /** * Response decorator, with an additional headers. @@ -48,7 +51,7 @@ public final class RsWithHeaders extends RsWrap { * @param res Original response * @param headers Headers */ - public RsWithHeaders(final Response res, final String... headers) { + public RsWithHeaders(final Response res, final CharSequence... headers) { this(res, Arrays.asList(headers)); } @@ -57,17 +60,22 @@ public RsWithHeaders(final Response res, final String... headers) { * @param res Original response * @param headers Headers */ - public RsWithHeaders(final Response res, final Iterable headers) { + public RsWithHeaders(final Response res, + final Iterable headers) { super( new Response() { @Override + @SuppressWarnings("unchecked") public Iterable head() throws IOException { return new Concat( res.head(), - new Transformer( - headers, - new Transformer.Trim() - ) + new Transformer( + new Transformer( + (Iterable)headers, + new Transformer.ToString() + ) + , new Transformer.Trim() + ) ); } @Override diff --git a/src/main/java/org/takes/rs/RsWithStatus.java b/src/main/java/org/takes/rs/RsWithStatus.java index f006cb41b..5bdd77d2e 100644 --- a/src/main/java/org/takes/rs/RsWithStatus.java +++ b/src/main/java/org/takes/rs/RsWithStatus.java @@ -76,7 +76,8 @@ public RsWithStatus(final Response res, final int code) { * @param code Status code * @param rsn Reason */ - public RsWithStatus(final Response res, final int code, final String rsn) { + public RsWithStatus(final Response res, final int code, + final CharSequence rsn) { super( new Response() { @Override @@ -101,7 +102,7 @@ public InputStream body() throws IOException { */ @SuppressWarnings("unchecked") private static Iterable head(final Response origin, - final int status, final String reason) throws IOException { + final int status, final CharSequence reason) throws IOException { // @checkstyle MagicNumber (1 line) if (status < 100 || status > 999) { throw new IllegalArgumentException( diff --git a/src/main/java/org/takes/rs/RsWithType.java b/src/main/java/org/takes/rs/RsWithType.java index ab236cfcd..4c54aaa8f 100644 --- a/src/main/java/org/takes/rs/RsWithType.java +++ b/src/main/java/org/takes/rs/RsWithType.java @@ -44,7 +44,7 @@ public final class RsWithType extends RsWrap { * @param res Original response * @param type Content type */ - public RsWithType(final Response res, final String type) { + public RsWithType(final Response res, final CharSequence type) { super( new RsWithHeader( new RsWithStatus(res, HttpURLConnection.HTTP_OK), diff --git a/src/main/java/org/takes/rs/RsWithoutHeader.java b/src/main/java/org/takes/rs/RsWithoutHeader.java index 029908731..e162f0ad8 100644 --- a/src/main/java/org/takes/rs/RsWithoutHeader.java +++ b/src/main/java/org/takes/rs/RsWithoutHeader.java @@ -49,14 +49,14 @@ public final class RsWithoutHeader extends RsWrap { * @param res Original response * @param name Header name */ - public RsWithoutHeader(final Response res, final String name) { + public RsWithoutHeader(final Response res, final CharSequence name) { super( new Response() { @Override @SuppressWarnings("unchecked") public Iterable head() throws IOException { final String prefix = String.format( - "%s:", name.toLowerCase(Locale.ENGLISH) + "%s:", name.toString().toLowerCase(Locale.ENGLISH) ); return new Concat( res.head(), diff --git a/src/main/java/org/takes/tk/TkVerbose.java b/src/main/java/org/takes/tk/TkVerbose.java index 67ac17f30..177e08182 100644 --- a/src/main/java/org/takes/tk/TkVerbose.java +++ b/src/main/java/org/takes/tk/TkVerbose.java @@ -60,7 +60,7 @@ public Response act(final Request request) throws IOException { ex.code(), String.format( "%s %s", - new RqMethod(request).method(), + new RqMethod.Base(request).method(), new RqHref.Base(request).href() ), ex diff --git a/src/test/java/org/takes/facets/auth/codecs/CcBase64Test.java b/src/test/java/org/takes/facets/auth/codecs/CcBase64Test.java new file mode 100644 index 000000000..f94ac9060 --- /dev/null +++ b/src/test/java/org/takes/facets/auth/codecs/CcBase64Test.java @@ -0,0 +1,140 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015 Yegor Bugayenko + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.takes.facets.auth.codecs; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.Map; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Ignore; +import org.junit.Test; +import org.takes.facets.auth.Identity; + +/** + * Test case for {@link CcBase64}. + * @author Igor Khvostenkov (ikhvostenkov@gmail.com) + * @version $Id$ + * @since 0.13 + */ +@Ignore +public final class CcBase64Test { + /** + * CcBase64 can encode. + * @throws IOException If some problem inside + */ + @Test + public void encodes() throws IOException { + MatcherAssert.assertThat( + new String( + new CcBase64(new CcPlain()).encode( + new Identity.Simple("urn:test:3") + ) + ), + Matchers.equalTo("dXJuJTNBdGVzdCUzQTM=") + ); + } + /** + * CcBase64 can decode. + * @throws IOException If some problem inside + */ + @Test + public void decodes() throws IOException { + MatcherAssert.assertThat( + new CcBase64(new CcPlain()).decode( + "dXJuJTNBdGVzdCUzQXRlc3Q=" + .getBytes() + ).urn(), + Matchers.equalTo("urn:test:test") + ); + } + /** + * CcBase64 can encode and decode. + * @throws IOException If some problem inside + */ + @Test + public void encodesAndDecodes() throws IOException { + final String urn = "urn:test:Hello World!"; + final Map properties = + ImmutableMap.of("userName", "user"); + final Codec codec = new CcBase64(new CcPlain()); + final Identity expected = codec.decode( + codec.encode(new Identity.Simple(urn, properties)) + ); + MatcherAssert.assertThat( + expected.urn(), + Matchers.equalTo(urn) + ); + MatcherAssert.assertThat( + expected.properties(), + Matchers.equalTo(properties) + ); + } + /** + * CcBase64 can encode empty byte array. + * @throws IOException If some problem inside + */ + @Test + public void encodesEmptyByteArray() throws IOException { + MatcherAssert.assertThat( + new String( + new CcBase64(new CcPlain()).encode( + new Identity.Simple("") + ) + ), + Matchers.equalTo("") + ); + } + /** + * CcBase64 can decode non Base64 alphabet symbols. + * @throws IOException If some problem inside + */ + @Test + public void decodesNonBaseSixtyFourAlphabetSymbols() throws IOException { + try { + new CcStrict(new CcBase64(new CcPlain())).decode( + " ^^^".getBytes() + ); + } catch (final DecodingException ex) { + MatcherAssert.assertThat( + ex.getMessage(), + Matchers.equalTo( + "Illegal character in Base64 encoded data. [32, 94, 94, 94]" + ) + ); + } + } + /** + * Checks CcBase64 equals method. + * @throws Exception If some problem inside + */ + @Test + public void equalsAndHashCodeEqualTest() throws Exception { + EqualsVerifier.forClass(CcBase64.class) + .suppress(Warning.TRANSIENT_FIELDS) + .verify(); + } +} diff --git a/src/test/java/org/takes/facets/auth/codecs/CcXORTest.java b/src/test/java/org/takes/facets/auth/codecs/CcXORTest.java new file mode 100644 index 000000000..3104b9ef7 --- /dev/null +++ b/src/test/java/org/takes/facets/auth/codecs/CcXORTest.java @@ -0,0 +1,66 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015 Yegor Bugayenko + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.takes.facets.auth.codecs; + +import java.io.IOException; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.takes.facets.auth.Identity; + +/** + * Test case for {@link CcXOR}. + * @author Dmitry Zaytsev (dmitry.zaytsev@gmail.com) + * @version $Id$ + * @since 0.13.7 + */ +public final class CcXORTest { + + /** + * CcXor can encode and decode. + * @throws IOException If some problem inside + */ + @Test + public void encodesAndDecodes() throws IOException { + final String urn = "urn:domain:9"; + final Codec codec = new CcXOR( + new Codec() { + @Override + public byte[] encode(final Identity identity) { + return identity.urn().getBytes(); + } + @Override + public Identity decode(final byte[] bytes) { + return new Identity.Simple(new String(bytes)); + } + }, + "secret" + ); + MatcherAssert.assertThat( + codec.decode(codec.encode(new Identity.Simple(urn))).urn(), + Matchers.equalTo(urn) + ); + } +} diff --git a/src/test/java/org/takes/facets/fork/TkProducesTest.java b/src/test/java/org/takes/facets/fork/TkProducesTest.java new file mode 100644 index 000000000..af6e74473 --- /dev/null +++ b/src/test/java/org/takes/facets/fork/TkProducesTest.java @@ -0,0 +1,104 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015 Yegor Bugayenko + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.takes.facets.fork; + +import com.google.common.base.Joiner; +import java.io.IOException; +import java.util.Arrays; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.takes.HttpException; +import org.takes.Response; +import org.takes.Take; +import org.takes.rq.RqFake; +import org.takes.rs.RsEmpty; +import org.takes.rs.RsJSON; +import org.takes.rs.RsPrint; +import org.takes.tk.TkEmpty; +import org.takes.tk.TkFixed; + +/** + * Test case for {@link TkProduces}. + * @author Eugene Kondrashev (eugene.kondrashev@gmail.com) + * @version $Id$ + * @since 0.14 + */ +public final class TkProducesTest { + + /** + * TkProduces can fail on unsupported Accept header. + * @throws IOException If some problem inside + */ + @Test(expected = HttpException.class) + public void failsOnUnsupportedAcceptHeader() throws IOException { + final Take produces = new TkProduces( + new TkEmpty(), + "text/json,application/json" + ); + produces.act( + new RqFake( + Arrays.asList( + "GET /hz0", + "Host: as0.example.com", + "Accept: text/xml" + ), + "" + ) + ).head(); + } + + /** + * TkProduce can produce correct type response. + * @throws IOException If some problem inside + */ + @Test + public void producesCorrectContentTypeResponse() throws IOException { + final Take produces = new TkProduces( + new TkFixed(new RsJSON(new RsEmpty())), + "text/json" + ); + final Response response = produces.act( + new RqFake( + Arrays.asList( + "GET /hz09", + "Host: as.example.com", + "Accept: text/json" + ), + "" + ) + ); + MatcherAssert.assertThat( + new RsPrint(response).print(), + Matchers.equalTo( + Joiner.on("\r\n").join( + "HTTP/1.1 200 OK", + "Content-Type: application/json", + "", + "" + ) + ) + ); + } +} diff --git a/src/test/java/org/takes/misc/IterableTransformTest.java b/src/test/java/org/takes/misc/IterableTransformTest.java index ed734e679..7ee0e8806 100644 --- a/src/test/java/org/takes/misc/IterableTransformTest.java +++ b/src/test/java/org/takes/misc/IterableTransformTest.java @@ -48,9 +48,9 @@ public void iterableTransform() { alist.add("b1"); alist.add("c1"); MatcherAssert.assertThat( - new Transformer( + new Transformer( alist, - new Transformer.Action() { + new Transformer.Action() { @Override public String transform(final String element) { return element.concat("t"); diff --git a/src/test/java/org/takes/rq/RqFormTest.java b/src/test/java/org/takes/rq/RqFormTest.java index eb99846f5..6135e43a0 100644 --- a/src/test/java/org/takes/rq/RqFormTest.java +++ b/src/test/java/org/takes/rq/RqFormTest.java @@ -43,7 +43,7 @@ public final class RqFormTest { */ @Test public void parsesHttpBody() throws IOException { - final RqForm req = new RqForm( + final RqForm req = new RqForm.Base( new RqFake( Arrays.asList( "GET /h?a=3", diff --git a/src/test/java/org/takes/rq/RqHrefTest.java b/src/test/java/org/takes/rq/RqHrefTest.java index f85d2e960..52c3787d2 100644 --- a/src/test/java/org/takes/rq/RqHrefTest.java +++ b/src/test/java/org/takes/rq/RqHrefTest.java @@ -138,7 +138,7 @@ public void extractsParamByDefault() throws IOException { "" ) ) - ).param("absent", "def-5"), + ).single("absent", "def-5"), Matchers.startsWith("def-") ); } diff --git a/src/test/java/org/takes/rq/RqMethodTest.java b/src/test/java/org/takes/rq/RqMethodTest.java index fb12ad94d..b1d841fd5 100644 --- a/src/test/java/org/takes/rq/RqMethodTest.java +++ b/src/test/java/org/takes/rq/RqMethodTest.java @@ -43,7 +43,7 @@ public final class RqMethodTest { @Test public void returnsMethod() throws IOException { MatcherAssert.assertThat( - new RqMethod(new RqFake(RqMethod.POST)).method(), + new RqMethod.Base(new RqFake(RqMethod.POST)).method(), Matchers.equalTo(RqMethod.POST) ); } diff --git a/src/test/java/org/takes/rq/RqMultipartTest.java b/src/test/java/org/takes/rq/RqMultipartTest.java index 965f6d5a1..6aee89245 100644 --- a/src/test/java/org/takes/rq/RqMultipartTest.java +++ b/src/test/java/org/takes/rq/RqMultipartTest.java @@ -35,7 +35,7 @@ import org.takes.Request; /** - * Test case for {@link RqMultipart}. + * Test case for {@link RqMultipart.Base}. * @author Yegor Bugayenko (yegor@teamed.io) * @version $Id$ * @since 0.9 @@ -49,7 +49,7 @@ public final class RqMultipartTest { */ private static final String CR = "\r\n"; /** - * RqMultipart can satisfy equals contract. + * RqMultipart.Base can satisfy equals contract. * @throws IOException if some problem inside */ @Test @@ -63,13 +63,13 @@ public void satisfiesEqualsContract() throws IOException { "Content-Disposition: form-data; name=\"data\"; filename=\"a.bin\"" ); MatcherAssert.assertThat( - new RqMultipart(req), - Matchers.equalTo(new RqMultipart(req)) + new RqMultipart.Base(req), + Matchers.equalTo(new RqMultipart.Base(req)) ); } /** - * RqMultipart can throw exception on no closing boundary found. + * RqMultipart.Base can throw exception on no closing boundary found. * @throws IOException if some problem inside */ @Test(expected = IOException.class) @@ -89,11 +89,11 @@ public void throwsExceptionOnNoClosingBounaryFound() throws IOException { "Content-Transfer-Encoding: uwf-8" ) ); - new RqMultipart(req); + new RqMultipart.Base(req); } /** - * RqMultipart can throw exception on no name + * RqMultipart.Base can throw exception on no name * at Content-Disposition header. * @throws IOException if some problem inside */ @@ -107,11 +107,12 @@ public void throwsExceptionOnNoNameAtContentDispositionHeader() "340 N Wolfe Rd, Sunnyvale, CA 94085" ) ); - new RqMultipart(req); + new RqMultipart.Base(req); } /** - * RqMultipart can throw exception on no boundary at Content-Type header. + * RqMultipart.Base can throw exception on no boundary + * at Content-Type header. * @throws IOException if some problem inside */ @Test(expected = IOException.class) @@ -126,11 +127,11 @@ public void throwsExceptionOnNoBoundaryAtContentTypeHeader() ), "" ); - new RqMultipart(req); + new RqMultipart.Base(req); } /** - * RqMultipart can throw exception on invalid Content-Type header. + * RqMultipart.Base can throw exception on invalid Content-Type header. * @throws IOException if some problem inside */ @Test(expected = IOException.class) @@ -144,11 +145,11 @@ public void throwsExceptionOnInvalidContentTypeHeader() throws IOException { ), "" ); - new RqMultipart(req); + new RqMultipart.Base(req); } /** - * RqMultipart can parse http body. + * RqMultipart.Base can parse http body. * @throws IOException If some problem inside */ @Test @@ -161,7 +162,7 @@ public void parsesHttpBody() throws IOException { ), "Content-Disposition: form-data; name=\"data\"; filename=\"a.bin\"" ); - final RqMultipart multi = new RqMultipart(req); + final RqMultipart.Base multi = new RqMultipart.Base(req); MatcherAssert.assertThat( new RqHeaders( multi.part("address").iterator().next() @@ -179,7 +180,7 @@ public void parsesHttpBody() throws IOException { } /** - * RqMultipart can return empty iterator on invalid part request. + * RqMultipart.Base can return empty iterator on invalid part request. * @throws IOException If some problem inside */ @Test @@ -192,7 +193,7 @@ public void returnsEmptyIteratorOnInvalidPartRequest() throws IOException { ), "Content-Disposition: form-data; name=\"data\"; filename=\"a.zip\"" ); - final RqMultipart multi = new RqMultipart(req); + final RqMultipart.Base multi = new RqMultipart.Base(req); MatcherAssert.assertThat( multi.part("fake").iterator().hasNext(), Matchers.is(false) @@ -200,7 +201,7 @@ public void returnsEmptyIteratorOnInvalidPartRequest() throws IOException { } /** - * RqMultipart can return correct name set. + * RqMultipart.Base can return correct name set. * @throws IOException If some problem inside */ @Test @@ -213,7 +214,7 @@ public void returnsCorrectNamesSet() throws IOException { ), "Content-Disposition: form-data; name=\"data\"; filename=\"a.bin\"" ); - final RqMultipart multi = new RqMultipart(req); + final RqMultipart.Base multi = new RqMultipart.Base(req); MatcherAssert.assertThat( multi.names(), Matchers.>equalTo( diff --git a/src/test/java/org/takes/rs/RsWithCookieTest.java b/src/test/java/org/takes/rs/RsWithCookieTest.java index d56d04a55..dbd1050a2 100644 --- a/src/test/java/org/takes/rs/RsWithCookieTest.java +++ b/src/test/java/org/takes/rs/RsWithCookieTest.java @@ -61,4 +61,21 @@ public void addsCookieToResponse() throws IOException { ); } + /** + * RsWithCookie can reject invalid cookie name. + * @throws IOException If some problem inside + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsInvalidName() throws IOException { + new RsWithCookie(new RsEmpty(), "f oo", "works"); + } + + /** + * RsWithCookie can reject invalid cookie value. + * @throws IOException If some problem inside + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsInvalidValue() throws IOException { + new RsWithCookie(new RsEmpty(), "bar", "wo\"rks"); + } }