/* * The MIT License * * Copyright 2013 Tim Boudreau. * * 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 NONINFRINGEMENT. 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 com.mastfrog.acteur; import com.google.common.net.MediaType; import com.google.inject.Provider; import com.google.inject.Singleton; import com.mastfrog.acteur.ResponseHeaders.ETagProvider; import com.mastfrog.acteur.errors.Err; import com.mastfrog.acteur.headers.Headers; import com.mastfrog.acteur.headers.Method; import com.mastfrog.acteur.preconditions.Description; import com.mastfrog.acteur.server.PathFactory; import com.mastfrog.acteur.util.HttpMethod; import com.mastfrog.acteurbase.Chain; import com.mastfrog.giulius.Dependencies; import com.mastfrog.url.Path; import com.mastfrog.util.Checks; import com.mastfrog.util.Exceptions; import com.mastfrog.util.Strings; import io.netty.handler.codec.http.HttpResponseStatus; import static io.netty.handler.codec.http.HttpResponseStatus.SEE_OTHER; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import javax.inject.Inject; import org.netbeans.validation.api.InvalidInputException; import org.netbeans.validation.api.Problem; /** * Factory for standard Acteur implementations, mainly used to determine if a * request is valid (matches a URL, is using a supported HTTP method, etc.). * Usage model: Ask for this in your {@link Page} constructor and use it to add * acteurs. * <i><b>Almost all methods on this class can be used via annotations, so using * this class directly is rare post Acteur 1.4</b></i>. * * @author Tim Boudreau */ @Singleton public class ActeurFactory { @Inject private Dependencies deps; @Inject private Charset charset; @Inject private PatternAndGlobCache cache; @Inject private Provider<HttpEvent> event; /** * Reject the request if it is not one of the passed HTTP methods * * @param methods Methods * @return An Acteur that can be used in a page * @deprecated Use @Methods instead - it is self-documenting */ @Deprecated public Acteur matchMethods(final Method... methods) { return matchMethods(false, methods); } /** * Reject the request if it is not an allowed HTTP method, optionally * including information in the response, or simply rejecting the request * and allowing the next page a crack at it. * * @param notSupp If true, respond with METHOD_NOT_ALLOWED and the ALLOW * header set * @param methods The http methods which are allowed * @return An Acteur */ public Acteur matchMethods(final boolean notSupp, final Method... methods) { if (methods.length == 1) { return new MatchMethod(event, notSupp, charset, methods[0]); } return new MatchMethods(event, notSupp, charset, methods); } private static class MatchMethods extends Acteur { private final Provider<HttpEvent> deps; private final boolean notSupp; private final Charset charset; private final Method[] methods; public MatchMethods(Provider<HttpEvent> deps, boolean notSupp, Charset charset, Method... methods) { this.deps = deps; this.notSupp = notSupp; this.charset = charset; this.methods = methods; } private boolean hasMethod(HttpMethod m) { for (Method mm : methods) { if (mm == m || mm.equals(m)) { return true; } } return false; } @Override public com.mastfrog.acteur.State getState() { HttpEvent event = deps.get(); boolean hasMethod = hasMethod(event.getMethod()); add(Headers.ALLOW, methods); if (notSupp && !hasMethod) { add(Headers.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.withCharset(charset)); return new Acteur.RespondWith(new Err(HttpResponseStatus.METHOD_NOT_ALLOWED, "405 Method " + event.getMethod() + " not allowed. Accepted methods are " + Headers.ALLOW.toString(methods) + "\n")); } com.mastfrog.acteur.State result = hasMethod ? new Acteur.ConsumedState() : new Acteur.RejectedState(); return result; } @Override public String toString() { return "Match Methods " + Arrays.asList(methods); } @Override public void describeYourself(Map<String, Object> into) { into.put("Methods", methods); } } private static class MatchMethod extends Acteur { private final Provider<HttpEvent> deps; private final boolean notSupp; private final Charset charset; private final Method[] method; public MatchMethod(Provider<HttpEvent> deps, boolean notSupp, Charset charset, Method... method) { this.deps = deps; this.notSupp = notSupp; this.charset = charset; this.method = method; } @Override public com.mastfrog.acteur.State getState() { HttpEvent event = deps.get(); HttpMethod mth = event.getMethod(); boolean hasMethod = mth == method[0] || method[0].equals(event.getMethod()); add(Headers.ALLOW, method); if (notSupp && !hasMethod) { add(Headers.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.withCharset(charset)); return new Acteur.RespondWith(new Err(HttpResponseStatus.METHOD_NOT_ALLOWED, "405 Method " + event.getMethod() + " not allowed. Accepted methods are " + Headers.ALLOW.toString(method) + "\n")); } com.mastfrog.acteur.State result = hasMethod ? new Acteur.ConsumedState() : new Acteur.RejectedState(); return result; } @Override public String toString() { return "Match Method " + method; } @Override public void describeYourself(Map<String, Object> into) { into.put("Method", method); } } public Acteur exactPathLength(final int length) { Checks.nonNegative("length", length); return new Acteur() { @Override public com.mastfrog.acteur.State getState() { HttpEvent event = ActeurFactory.this.event.get(); if (event.getPath().getElements().length == length) { return new RejectedState(); } else { return new ConsumedState(); } } @Override public void describeYourself(Map<String, Object> into) { into.put("Path-element-count", length); } }; } public Acteur minimumPathLength(final int length) { Checks.nonZero("length", length); Checks.nonNegative("length", length); return new Acteur() { @Override public com.mastfrog.acteur.State getState() { HttpEvent event = ActeurFactory.this.event.get(); if (event.getPath().getElements().length < length) { return new RejectedState(); } else { return new ConsumedState(); } } @Override public void describeYourself(Map<String, Object> into) { into.put("Minimum Path Length", length); } }; } public Acteur maximumPathLength(final int length) { Checks.nonZero("length", length); Checks.nonNegative("length", length); return new Acteur() { @Override public com.mastfrog.acteur.State getState() { HttpEvent event = ActeurFactory.this.event.get(); if (event.getPath().getElements().length > length) { return new Acteur.RejectedState(); } else { return new Acteur.ConsumedState(); } } @Override public void describeYourself(Map<String, Object> into) { into.put("Maximum Path Length", length); } }; } public Acteur redirect(String location) throws URISyntaxException { return redirect(location, HttpResponseStatus.SEE_OTHER); } public Acteur redirect(String location, HttpResponseStatus status) throws URISyntaxException { Checks.notNull("location", location); Checks.notNull("status", status); switch (status.code()) { case 300: case 301: case 302: case 303: case 305: case 307: break; default: throw new IllegalArgumentException(status + " is not a redirect"); } return new Redirect(location, status); } private static final class Redirect extends Acteur { private final URI location; private final HttpResponseStatus status; private Redirect(String location, HttpResponseStatus status) throws URISyntaxException { this.location = new URI(location); this.status = status; } public com.mastfrog.acteur.State getState() { add(Headers.LOCATION, location); return new RespondWith(status, "Redirecting to " + location); } } /** * Creates an Acteur which will read the request body, construct an object * from it, and include it for injection into later Acteurs in the chain. * * @param <T> * @param type The object type * @return An acteur */ public <T> Acteur injectRequestBodyAsJSON(final Class<T> type) { return new InjectBody<T>(deps, type); } @Description("Injects the body as a specific type") private static final class InjectBody<T> extends Acteur { private final Dependencies deps; private final Class<T> type; InjectBody(Dependencies deps, Class<T> type) { this.deps = deps; this.type = type; } @Override public com.mastfrog.acteur.State getState() { final ContentConverter converter = deps.getInstance(ContentConverter.class); HttpEvent evt = deps.getInstance(HttpEvent.class); try { MediaType mt = evt.getHeader(Headers.CONTENT_TYPE); if (mt == null) { mt = MediaType.ANY_TYPE; } try { T obj = converter.readObject(evt.getContent(), mt, type); return new Acteur.ConsumedLockedState(obj); } catch (InvalidInputException e) { List<String> pblms = new LinkedList<>(); for (Problem p : e.getProblems()) { pblms.add(p.getMessage()); } return new Acteur.RespondWith(Err.badRequest("Invalid data").put("problems", pblms)); } } catch (IOException ex) { // Logger.getLogger(ActeurFactory.class.getName()).log(Level.SEVERE, null, ex); return new Acteur.RespondWith(Err.badRequest("Bad or no JSON\n" + stackTrace(ex))); } } @Override public void describeYourself(Map<String, Object> into) { into.put("Expects JSON Request Body", true); } } private static String stackTrace(Throwable t) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos); t.printStackTrace(ps); return new String(baos.toByteArray()); } /** * Create an acteur which will take the request parameters, turn them into * an implementation of some interface and include that in the set of * objects later Acteurs in the chain can request for injection. * <p/> * Note that you may need to include the type in your application's * <code>@ImplicitBindings</code> annotation for Guice to allow your * type to be injected. * <p/> * The type must be an interface type, and its methods should correspond * exactly to the parameter names desired. */ public <T> Acteur injectRequestParametersAs(final Class<T> type) { return new InjectParams<T>(deps, type); } @Description("Inject request parameters as a type") static class InjectParams<T> extends Acteur { private final Dependencies deps; private final Class<T> type; InjectParams(Dependencies deps, Class<T> type) { this.deps = deps; this.type = type; } @Override public com.mastfrog.acteur.State getState() { HttpEvent evt = deps.getInstance(HttpEvent.class); ContentConverter converter = deps.getInstance(ContentConverter.class); try { T obj = converter.createObjectFor(evt.getParametersAsMap(), type); if (obj != null) { return new Acteur.ConsumedLockedState(obj); } } catch (InvalidInputException ex) { setState(new Acteur.RespondWith(Err.badRequest(ex.getProblems().toString()))); } return new Acteur.RejectedState(); } @Override public void describeYourself(Map<String, Object> into) { into.put("type", type.getName()); } public String toString() { return "Inject request parameters as " + type.getName(); } } /** * Create an Acteur which simply always responds with the given HTTP status. * * @param status A status * @return An acteur */ public Acteur responseCode(final HttpResponseStatus status) { @Description("Send a response code") class SendResponseCode extends Acteur { @Override public com.mastfrog.acteur.State getState() { return new Acteur.RespondWith(status); } @Override public void describeYourself(Map<String, Object> into) { into.put("Responds With", status); } } return new SendResponseCode(); } /** * Reject the request unless certain URL parameters are present * * @param names The parameter names * @return An acteur */ public Acteur requireParameters(final String... names) { return new RequireParameters(event, charset, names); } static class RequireParameters extends Acteur { private final Provider<HttpEvent> deps; private final Charset charset; private final String[] names; public RequireParameters(Provider<HttpEvent> deps, Charset charset, String... names) { this.deps = deps; this.charset = charset; this.names = names; } @Override public com.mastfrog.acteur.State getState() { HttpEvent event = deps.get(); for (String nm : names) { String val = event.getParameter(nm); if (val == null) { add(Headers.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.withCharset(charset)); return new Acteur.RespondWith(Err.badRequest("Missing URL parameter '" + nm + "'\n")); } } return new Acteur.ConsumedState(); } @Override public String toString() { return "Require Parameters " + Arrays.asList(names); } @Override public void describeYourself(Map<String, Object> into) { into.put("requiredParameters", names); } } public Acteur parametersMayNotBeCombined(final String... names) { @Description("Requires that parameters not appear together in the URL") class RequireParametersNotBeCombined extends Acteur { @Override public com.mastfrog.acteur.State getState() { HttpEvent event = ActeurFactory.this.event.get(); String first = null; for (String nm : names) { String val = event.getParameter(nm); if (val != null) { if (first == null) { first = nm; } else { add(Headers.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.withCharset(charset)); return new Acteur.RespondWith(Err.badRequest( "Parameters may not contain both '" + first + "' and '" + nm + "'\n")); } } } return new Acteur.ConsumedState(); } @Override public String toString() { return "Parameters may not be combined: " + Strings.toString(Arrays.asList(names)); } @Override public void describeYourself(Map<String, Object> into) { into.put("requiredParameters", names); } } return new RequireParametersNotBeCombined(); } public Acteur parametersMustBeNumbersIfTheyArePresent(final boolean allowDecimal, final boolean allowNegative, final String... names) { @Description("Requires that parameters be numbers if they are present") class NumberParameters extends Acteur { @Override public void describeYourself(Map<String, Object> into) { into.put("URL parameters must be numbers if present" + (allowNegative ? "(negative allowed) " : ("(must be non-negative) ")) + (allowDecimal ? "(decimal-allowed)" : "(must be integers)") + (""), names); } public com.mastfrog.acteur.State getState() { HttpEvent evt = event.get(); for (String name : names) { String p = evt.getParameter(name); if (p != null) { boolean decimalSeen = false; for (int i = 0; i < p.length(); i++) { switch (p.charAt(i)) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': break; case '-': if (i == 0 && allowNegative) { break; } //fall thru case '.': if (!decimalSeen && allowDecimal) { decimalSeen = true; break; } //fall thru default: return new RespondWith(Err.badRequest( "Parameter " + name + " is not a legal number here: '" + p + "'\n")); } } } } return new Acteur.ConsumedState(); } } return new NumberParameters(); } /** * Reject request which contain the passed parameters * * @param names A list of parameter names for the URL * @return An acteur */ public Acteur banParameters(final String... names) { Arrays.sort(names); @Description("Requires that parameters not be present") class BanParameters extends Acteur { public com.mastfrog.acteur.State getState() { HttpEvent evt = event.get(); for (Map.Entry<String, String> e : evt.getParametersAsMap().entrySet()) { if (Arrays.binarySearch(names, e.getKey()) >= 0) { return new RespondWith(Err.badRequest( e.getKey() + " not allowed in parameters\n")); } } return new ConsumedState(); } @Override public void describeYourself(Map<String, Object> into) { into.put("Illegal Parameters", names); } } return new BanParameters(); } /** * Reject the request if none of the passed parameter names are present * * @param names * @return */ public Acteur requireAtLeastOneParameter(final String... names) { @Description("Requires that at least one specified parameter be present") class RequireAtLeastOneParameter extends Acteur { @Override public com.mastfrog.acteur.State getState() { HttpEvent event = ActeurFactory.this.event.get(); for (String nm : names) { String val = event.getParameter(nm); if (val != null) { return new ConsumedState(); } } StringBuilder sb = new StringBuilder(); for (int i = 0; i < names.length; i++) { sb.append("'").append(names[i]).append("'"); if (i != names.length - 1) { sb.append(", "); } } return new RespondWith(Err.badRequest("Must have at least one of " + sb + " as parameters\n")); } @Override public void describeYourself(Map<String, Object> into) { into.put("At least one parameter required", names); } @Override public String toString() { return "Require Parameters " + Arrays.asList(names); } } return new RequireAtLeastOneParameter(); } /** * Reject the request if HttpEvent.getPath().toString() does not match one * of the passed regular expressions * * @param regexen Regexen * @return An acteur * @deprecated Use @PathRegex instead - it is self-documenting */ @Deprecated public Acteur matchPath(final String... regexen) { if (regexen.length == 1) { String exactPath = cache.exactPathForRegex(regexen[0]); if (exactPath != null) { return new ExactMatchPath(event, exactPath); } } return new MatchPath(event, cache, regexen); } static class ExactMatchPath extends Acteur { private final String path; private final Provider<HttpEvent> deps; ExactMatchPath(Provider<HttpEvent> deps, String path) { this.path = path.length() > 1 && path.charAt(0) == '/' ? path.substring(1) : path; this.deps = deps; } @Override public com.mastfrog.acteur.State getState() { HttpEvent event = deps.get(); if (path.equals(event.getPath().toString())) { return new ConsumedState(); } return new RejectedState(); } @Override public void describeYourself(Map<String, Object> into) { into.put("Exactly match the URL path", path); } @Override public String toString() { return "Exactly match the URL path " + path; } } static final class MatchPath extends Acteur { private final Provider<HttpEvent> deps; private final PatternAndGlobCache cache; private final String[] regexen; MatchPath(Provider<HttpEvent> deps, PatternAndGlobCache cache, String... regexen) { if (regexen.length == 0) { throw new IllegalArgumentException("No regular expressions provided"); } this.deps = deps; this.cache = cache; this.regexen = regexen; } @Override public com.mastfrog.acteur.State getState() { HttpEvent event = deps.get(); for (String regex : regexen) { Pattern p = cache.getPattern(regex); boolean matches = p.matcher(event.getPath().toString()).matches(); if (matches) { return new ConsumedState(); } } return new RejectedState(); } @Override public void describeYourself(Map<String, Object> into) { into.put("URL Patterns", regexen); } @Override public String toString() { return "Match path " + Arrays.asList(regexen); } } @Singleton static class PatternAndGlobCache { private final Map<String, Boolean> matchCache = new ConcurrentHashMap<>(); private final Map<String, String> exactPathForRegex = new ConcurrentHashMap<>(); private static final String INVALID = "::////"; String exactPathForRegex(String regex) { String result = exactPathForRegex.get(regex); if (result != null) { if (!INVALID.equals(result)) { return result; } else { return null; } } StringBuilder sb = new StringBuilder(); char[] chars = regex.toCharArray(); boolean precedingWasBackslash = false; boolean endMarkerFound = false; boolean startMarkerFound = false; loop: for (int i = 0; i < chars.length; i++) { char c = chars[i]; if (i == 0 && c == '^') { continue; } if (i == chars.length - 1 && c == '$') { endMarkerFound = true; continue; } if (i == 0 && c == '^') { startMarkerFound = true; } switch (c) { case '\\': if (i != chars.length - 1) { precedingWasBackslash = true; } break; case '*': if (precedingWasBackslash) { sb.append(c); continue; } case '[': case '+': case '?': case '^': case '$': case '&': exactPathForRegex.put(regex, INVALID); return null; default : sb.append(c); } precedingWasBackslash = c == '\\'; } if (!endMarkerFound || !startMarkerFound) { exactPathForRegex.put(regex, INVALID); return null; } if (sb.length() > 0 && sb.charAt(0) == '/') { result = sb.substring(1); } else { result = sb.toString(); } exactPathForRegex.put(regex, result); return result; } boolean isExactGlob(String s) { Boolean match = matchCache.get(s); if (match != null) { return match.booleanValue(); } boolean result = true; for (char c : s.toCharArray()) { if ('*' == c) { result = false; break; } } matchCache.put(s, result); return result; } private final Map<String, Pattern> patternCache = new ConcurrentHashMap<>(); Pattern getPattern(String regex) { Pattern result = patternCache.get(regex); if (result == null) { result = Pattern.compile(regex); patternCache.put(regex, result); } return result; } } /** * Checks the IF_NONE_MATCH header and compares it with the value from the * current Page's getETag() method. If it matches, forces a NOT_MODIFIED * http response and ends processing of the chain of Acteurs. * * @return An acteur */ public Acteur sendNotModifiedIfETagHeaderMatches() { return Acteur.wrap(CheckIfNoneMatchHeader.class, deps); } public Class<? extends Acteur> sendNotModifiedIfETagHeaderMatchesType() { return CheckIfNoneMatchHeader.class; } static String patternFromGlob(String pattern) { if (pattern.length() > 0 && pattern.charAt(0) == '/') { pattern = pattern.substring(1); } StringBuilder match = new StringBuilder("^\\/?"); for (char c : pattern.toCharArray()) { switch (c) { case '$': case '.': case '{': case '}': case '[': case ']': case ')': case '(': case '^': case '/': match.append("\\" + c); break; case '*': match.append("[^\\/]*?"); break; case '?': match.append("[^\\/]?"); break; default: match.append(c); } } match.append("$"); return match.toString(); } public Acteur globPathMatch(String... patterns) { if (patterns.length == 1 && cache.isExactGlob(patterns[0])) { String pattern = patterns[0]; if (pattern.length() > 0 && pattern.charAt(0) == '/') { pattern = pattern.substring(1); } return new ExactMatchPath(event, patterns[0]); } String[] rexen = new String[patterns.length]; for (int i = 0; i < rexen.length; i++) { rexen[i] = patternFromGlob(patterns[i]); } return matchPath(rexen); } /** * Check the "If-Modified-Since" header and compares it to the current * Page's getLastModified (rounding milliseconds down). If the condition is * met, responds with NOT_MODIFIED. * * @return an Acteur */ public Acteur sendNotModifiedIfIfModifiedSinceHeaderMatches() { return Acteur.wrap(CheckIfModifiedSinceHeader.class, deps); } public Class<? extends Acteur> sendNotModifiedIfIfModifiedSinceHeaderMatchesType() { return CheckIfModifiedSinceHeader.class; } /** * Check the "If-Unmodified-Since" header * * @return an Acteur */ public Acteur preconditionFailedIfUnmodifiedSinceMatches() { return Acteur.wrap(CheckIfUnmodifiedSinceHeader.class, deps); } public Class<? extends Acteur> preconditionFailedIfUnmodifiedSinceMatchesType() { return CheckIfUnmodifiedSinceHeader.class; } public Acteur respondWith(int status) { return new ResponseCode(status); } public Acteur respondWith(int status, String msg) { return new ResponseCode(status, msg); } public Acteur respondWith(HttpResponseStatus status, String msg) { return new ResponseCode(status, msg); } public Acteur respondWith(HttpResponseStatus status) { return new ResponseCode(status); } private static final class ResponseCode extends Acteur { private final HttpResponseStatus code; private final String msg; public ResponseCode(int code) { this(code, null); } public ResponseCode(int code, String msg) { this(HttpResponseStatus.valueOf(code), msg); } public ResponseCode(HttpResponseStatus code, String msg) { this.code = code; this.msg = msg; } public ResponseCode(HttpResponseStatus code) { this(code, null); } @Override public void describeYourself(Map<String, Object> into) { into.put("Always responds with", code.code() + " " + code.reasonPhrase()); } @Override public com.mastfrog.acteur.State getState() { return msg == null ? new RespondWith(code) : new RespondWith(code, msg); } } public Acteur minimumBodyLength(final int length) { return new Acteur() { @Override public com.mastfrog.acteur.State getState() { try { int val = event.get().getContent().readableBytes(); if (val < length) { return new RespondWith(Err.badRequest("Request body must be > " + length + " characters")); } return new ConsumedState(); } catch (IOException ex) { return Exceptions.chuck(ex); } } }; } public Acteur maximumBodyLength(final int length) { return new Acteur() { @Override public com.mastfrog.acteur.State getState() { try { int val = event.get().getContent().readableBytes(); if (val > length) { return new Acteur.RespondWith(new Err(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, "Request body must be < " + length + " characters")); } return new Acteur.ConsumedState(); } catch (IOException ex) { return Exceptions.chuck(ex); } } }; } /** * Compute the etag on demand, and send a not modified header if the one in * the request matches the one provided by the passed ETagComputer. * <p> * Note this depends on the etag being set on the Page, not just passed as * an earlier header. * * @param computer The thing that computes an ETag * @return An acteur */ public Acteur sendNotModifiedIfETagHeaderMatches(final ETagComputer computer) { class A extends Acteur implements ETagProvider { @Override public com.mastfrog.acteur.State getState() { Page page = deps.getInstance(Page.class); page.getResponseHeaders().setETagProvider(this); CheckIfNoneMatchHeader h = deps.getInstance(CheckIfNoneMatchHeader.class); com.mastfrog.acteur.State result = h.getState(); getResponse().merge(h.getResponse()); return result; } @Override public String getETag() { try { return computer.getETag(); } catch (Exception ex) { return Exceptions.<String>chuck(ex); } } @Override public void describeYourself(Map<String, Object> into) { into.put("Supports If-None-Match", true); } } return new A(); } public Acteur requireParametersIfMethodMatches(final Method method, final String... params) { Checks.notNull("method", method); Checks.notNull("params", params); Checks.notEmpty("params", Arrays.asList(params)); class RequireParametersIfMethodMatches extends Acteur { public com.mastfrog.acteur.State getState() { HttpEvent evt = event.get(); if (method.equals(evt.getMethod())) { if (!evt.getParametersAsMap().keySet().containsAll(Arrays.asList(params))) { return new RespondWith(Err.badRequest("Required parameters: " + Arrays.asList(params))); } } return new ConsumedState(); } } return new RequireParametersIfMethodMatches(); } public Acteur redirectEmptyPath(final String to) throws URISyntaxException { return redirectEmptyPath(Path.parse(to)); } public Acteur redirectEmptyPath(final Path to) throws URISyntaxException { Checks.notNull("to", to); class MatchNothing extends Acteur { public com.mastfrog.acteur.State getState() { HttpEvent evt = event.get(); if (evt.getPath().toString().isEmpty()) { PathFactory pf = deps.getInstance(PathFactory.class); add(Headers.LOCATION, pf.toExternalPath(to).toURI()); return new RespondWith(SEE_OTHER); } else { return new RejectedState(); } } } return new MatchNothing(); } public Acteur branch(final Class<? extends Acteur> ifTrue, final Class<? extends Acteur> ifFalse, final Test test) { class Brancher extends Acteur { @Override @SuppressWarnings("unchecked") public com.mastfrog.acteur.State getState() { boolean result = test.test(event.get()); Chain<Acteur> chain = deps.getInstance(Chain.class); if (result) { chain.add(ifTrue); } else { chain.add(ifFalse); } return new ConsumedLockedState(); } } return new Brancher(); } /** * A test which can be performed on a request, for example, to decide about * branching */ public interface Test { /** * Perform the test * * @param evt The request * @return The result of the test */ public boolean test(HttpEvent evt); } /** * Lazily computes a page's (or something's) etag */ public interface ETagComputer { /** * Compute the etag for whatever context we are called in * * @return * @throws Exception if something goes wrong */ public String getETag() throws Exception; } }