Package dk.brics.jwig

Source Code of dk.brics.jwig.WebContext

package dk.brics.jwig;

import dk.brics.jwig.server.Config;
import dk.brics.jwig.server.Dispatcher;
import dk.brics.jwig.server.RequestManager;
import dk.brics.jwig.server.ThreadContext;
import dk.brics.jwig.server.cache.Cache;
import dk.brics.jwig.server.cache.DependencyMap;
import dk.brics.jwig.util.Base64;
import dk.brics.xact.XML;
import org.apache.log4j.Logger;

import javax.mail.*;
import javax.servlet.ServletContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
* Context for web execution.
*/
abstract public class WebContext {

    /**
     * Priority for the main web method.
     *
     * @see #main()
     */
    public static final int MAX_PRIORITY = Integer.MAX_VALUE;

    /**
     * Priority for the server cache web method.
     *
     * @see #cache()
     */
    public static final int CACHE_PRIORITY = 500;

    /**
     * Priority to use for web methods to be executed before the server cache,
     * such as authentication methods.
     */
    public static final int PRE_CACHE = 1000;

    /**
     * Priority to use for handlers.
     *
     * @see #handlers()
     */
    public static final int HANDLERS = 0;
    static final String JWIG_DEFUNCT_SESSION = "X-JWIG-defunct-session";
    private static final String JWIG_REGENERATE = "X-JWIG_regenerate";

    /**
     * Logger.
     */
    public final Logger log = Logger.getLogger(WebContext.class);

    public static ServletContext context;

    /**
     * A map that for each client that has had a failed request saves the number
     * of consecutive failed requests.
     */
    private static final ConcurrentHashMap<String, AtomicInteger> failedRequestMap = new ConcurrentHashMap<String, AtomicInteger>();

    /**
     * A map that allows sessions to carry handlers from one request to another. Made a map on WebContext to
     * ensure that this data is not available to the client code that uses the session.
     */
    private final WeakHashMap<Session, Set<AbstractHandler>> sessionHandlers = new WeakHashMap<Session, Set<AbstractHandler>>();

    private final boolean auto_refresh_sessions;

    /**
     * Constructs a new empty web context object.
     */
    WebContext() {
        auto_refresh_sessions = Config.get("jwig.auto_refresh_sessions", true);

    }

    /**
     * Starts the timer that will reduce punishment for clients every five
     * minutes.
     */
    static {
        final Timer timer = new Timer(true);
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                try {
                    Logger log = Logger.getLogger(WebContext.class);
                    for (Iterator<Map.Entry<String, AtomicInteger>> it = failedRequestMap
                            .entrySet().iterator(); it.hasNext();) {
                        Map.Entry<String, AtomicInteger> entry = it.next();
                        log.info("Reducing punishment for " + entry.getKey());
                        AtomicInteger atomicInteger = entry.getValue();
                        atomicInteger.decrementAndGet();
                        if (atomicInteger.get() == 0) {
                            log.info("Forgiving " + entry.getKey());
                            it.remove();
                        }
                    }
                } catch (Exception e) {
                    Logger.getLogger(WebContext.class).info(
                            "Punishing timer stopped");
                    timer.cancel();
                }
            }
        }, 100000, 1000 * 60 * 5);
    }

    /**
     * Returns the <code>HttpServletRequest</code> object.
     */
    public static HttpServletRequest getServletRequest() {
        return ThreadContext.get().getServletRequest();
    }

    /**
     * Returns the <code>HttpServletResponse</code> object.
     */
    public static HttpServletResponse getServletResponse() {
        return ThreadContext.get().getServletResponse();
    }

    /**
     * Returns the <code>ServletContext</code> object.
     */
    public static ServletContext getServletContext() {
        return context;
    }

    /**
     * Sends the current response. Use in filter web methods (i.e. only GET
     * requests) for producing response. If no response has been sent and the
     * method has return type void, then the next web method in the chain is
     * invoked when the filter web method returns. Also used for producing a
     * temporary response and continuing execution.
     */
    public static void sendResponse() {
        ThreadContext c = ThreadContext.get();
        ThreadContext.getDispatcher().send(c.getResponse());
    }

    /**
     * Returns the current response object.
     */
    public static Response getResponse() {
        return ThreadContext.get().getResponse();
    }

    /**
     * Returns the URL of the current request.
     */
    public static URL getRequestURL() {
        try {
            return new URL(ThreadContext.get().getRequestURL());
        } catch (MalformedURLException e) {
            throw new JWIGException(e);
        }
    }

    /**
     * Generates a URL for the given web method within the same web site. The
     * URL uses <code>https</code> if the current request is on a secure
     * connection.
     *
     * @param method
     *            the name of the web method (optionally qualified by the class
     *            name, default: this web app)
     * @param args
     *            arguments to the web method. These arguments will be encoded using <code>toString</code> and will be part of
     *            the request URL for the web method. The list of args must match the list of formal parameters in the web method
     *            both in terms of number and types. The parameter will be mapped to names based on their order.
     */
    public static URL makeURL(String method, Object... args) {
        return makeURL(ThreadContext.get().getWebAppParams(), method, args);
    }

    /**
     * Generates a URL for the given web method within the same web site, with
     * web app arguments. The URL uses <code>https</code> if the current request
     * is on a secure connection.
     *
     * @param webapp_args
     *            web app arguments. This map can define values for arguments defined by the WebApp class (rather than the web method).
     *            See section 4.3 in the manual for details
     * @param method
     *            the name of the web method (optionally qualified by the class
     *            name, default: this web app)
     * @param args
     *            arguments to the web method. These arguments will be encoded using <code>toString</code> and will be part of the
     *            request URL for the web method. The list of args must match the list of formal parameters in the web method both in
     *            terms of number and types. The parameter will be mapped to names based on their order.
     */
    public static URL makeURL(Map<String, ?> webapp_args, String method,
            Object... args) {
        return ThreadContext.get().getRequestManager()
                .makeURL(webapp_args, method, args);
    }

    /**
     * Generates a URL for the given web method within the same web site and in
     * the given web app. The URL uses <code>https</code> if the current request
     * is on a secure connection.
     *
     * @param cl
     *            the web app to search for the method in
     * @param method
     *            the name of the web method
     * @param args
     *            arguments to the web method. These arguments will be encoded using <code>toString</code> and
     *            will be part of the request URL for the web method. The list of args must match the list of formal parameters
     *            in the web method both in terms of number and types. The parameter will be mapped to names based on their order.
     */
    public static URL makeURL(Class<? extends WebApp> cl, String method,
            Object... args) {
        return makeURL(cl.getCanonicalName() + "." + method, args);
    }

    /**
     * Generates a URL for the given web method within the same web site.
     *
     * @param secure
     *            if true, use <code>https</code>, otherwise <code>http</code>
     * @param method
     *            the name of the web method (optionally qualified by the class
     *            name, default: this web app)
     * @param args
     *            arguments to the web method. These arguments will be encoded using <code>toString</code> and will be part
     *            of the request URL for the web method. The list of args must match the list of formal parameters in the web method
     *            both in terms of number and types. The parameter will be mapped to names based on their order.
     */
    public static URL makeURL(boolean secure, String method, Object... args) {
        return makeURL(secure, ThreadContext.get().getWebAppParams(), method,
                args);
    }

    /**
     * Generates a URL for the given web method within the same web site, with
     * web app arguments.
     *
     * @param secure
     *            if true, use <code>https</code>, otherwise <code>http</code>
     * @param webapp_args
     *            web app arguments. This map can define values for arguments defined by the WebApp class (rather than the web method).
     *            See section 4.3 in the manual for details
     * @param method
     *            the name of the web method (optionally qualified by the class
     *            name, default: this web app)
     * @param args
     *            arguments to the web method. These arguments will be encoded using <code>toString</code> and will be part of the
     *            request URL for the web method. The list of args must match the list of formal parameters in the web method both
     *            in terms of number and types. The parameter will be mapped to names based on their order.
     */
    public static URL makeURL(boolean secure, Map<String, ?> webapp_args,
            String method, Object... args) {
        return ThreadContext.get().getRequestManager()
                .makeURL(secure, webapp_args, method, args);
    }

    /**
     * Generates a URL for the given web method within the same web site and in
     * the given web app.
     *
     * @param secure
     *            if true, use <code>https</code>, otherwise <code>http</code>
     * @param cl
     *            the web app to search for the method in
     * @param method
     *            the name of the web method
     * @param args
     *            arguments to the web method. These arguments will be encoded using <code>toString</code> and will be part of the
     *            request URL for the web method. The list of args must match the list of formal parameters in the web method both
     *            in terms of number and types. The parameter will be mapped to names based on their order.
     */
    public static URL makeURL(boolean secure, Class<? extends WebApp> cl,
            String method, Object... args) {
        return makeURL(secure, cl, ThreadContext.get().getWebAppParams(),
                       method, args);
    }

    /**
     * Generates a URL for the given web method within the same web site and in
     * the given web app, with web app arguments.
     *
     * @param secure
     *            if true, use <code>https</code>, otherwise <code>http</code>
     * @param cl
     *            the web app to search for the method in
     * @param webapp_args
     *            web app arguments. This map can define values for arguments defined by the WebApp class (rather than the web method).
     *            See section 4.3 in the manual for details
     * @param method
     *            the name of the web method
     * @param args
     *            arguments to the web method. These arguments will be encoded using <code>toString</code> and will be part of the
     *            request URL for the web method. The list of args must match the list of formal parameters in the web method both
     *            in terms of number and types. The parameter will be mapped to names based on their order.
     */
    public static URL makeURL(boolean secure, Class<? extends WebApp> cl,
            Map<String, ?> webapp_args, String method, Object... args) {
        return makeURL(secure, webapp_args, cl.getCanonicalName() + "."
                + method, args);
    }

    /**
     * Generates a URL for the root of the web site.
     * If the calling thread is bound to a request this method will return the URL for the WebSite either
     * from the configuration (if available) or (otherwise by inspecting the current request.
     * Otherwise if the method is called without a request context, this method will only inspect the configuration.
     * <br/>
     * The result of calling this method will be the same with or without a request context if the <tt>jwig.base_url</tt> and
     * <tt>jwig.base_url_secure</tt> properties have been set in <tt>jwig.properties</tt>.
     *
     * @param secure
     *            if true, use <code>https</code>, otherwise <code>http</code>
     */
    public static URL getWebSiteURL(boolean secure) {
        try {
            if (!ThreadContext.isInRequestContext()) {
                return new URL(ThreadContext.getBaseURL(secure));
            }
            return new URL(ThreadContext.get().getRequestManager()
                                        .getWebSiteURL(secure));
        } catch (MalformedURLException e) {
            throw new JWIGException(e);
        }
    }

    /**
     * Generates a URL for the root of this website. The URL uses https iff the
     * request does.
     * Requires the calling thread to be bound to a request.
     */
    public static URL getWebSiteURL() {
        return getWebSiteURL(ThreadContext.get().getServletRequest().isSecure());
    }

    /**
     * Generates a URL for the root of the current web application.
     *
     * @param secure
     *            if true, use <code>https</code>, otherwise <code>http</code>
     */
    public static URL getWebAppURL(boolean secure) {
        try {
            Map<String, String> stringMap = ThreadContext.get()
                    .getWebAppParams();
            Map<String, String[]> stringMap2 = new HashMap<String, String[]>();
            for (Map.Entry<String, String> e : stringMap.entrySet()) {
                stringMap2.put(e.getKey(), new String[] { e.getValue() });
            }
            return new URL(ThreadContext.get().getRequestManager()
                    .getWebAppURL(secure, stringMap2));
        } catch (MalformedURLException e) {
            throw new JWIGException(e);
        }
    }

    /**
     * Registers the given object as observable for the response. This means
     * that if the object is updated (see @link #update(Object)), the response
     * is invalidated in the cache.
     */
    public static void addResponseInvalidator(Object obj) {
        if (obj == null) {
            return;
        }
        DependencyMap dependencyMap = ThreadContext.getDependencyMap();
        if (dependencyMap != null) {
            dependencyMap.addResponseDependency(obj);
        }
    }

    /**
     * Informs observers that the given object has been updated.
     *
     * @see XMLProducer
     * @see #addResponseInvalidator(Object)
     */
    public static void update(Object obj) {
        if (obj == null) {
            return;
        }
        DependencyMap dependencyMap = ThreadContext.getDependencyMap();
        if (dependencyMap != null) {
            dependencyMap.objectUpdated(obj);
        }
    }

    /**
     * Sends an email using <a target="_top"
     * href="http://java.sun.com/products/javamail/">JavaMail</a>. All
     * configuration properties starting with <code>mail.</code> are used for
     * setting up a JavaMail session when the web app is initialized.
     *
     * @return a <code>SendFailedException</code> or null if there is no error
     * @throws JWIGException
     *             in case of other errors than <code>SendFailedException</code>
     * @see WebApp#setProperty(String, Object)
     * @see #sendEmails(List)
     */
    public SendFailedException sendEmail(Email message) throws JWIGException {
        List<Email> messages = new ArrayList<Email>();
        messages.add(message);
        return sendEmails(messages).get(0);
    }

    /**
     * Sends a collection of emails.
     *
     * @return a list of <code>SendFailedException</code>s, one for each message
     *         being sent, with null meaning "no error"
     * @throws JWIGException
     *             in case of other errors than <code>SendFailedException</code>
     * @see #sendEmail(Email)
     */
    public List<SendFailedException> sendEmails(List<Email> messages)
            throws JWIGException {
        try {
            List<SendFailedException> results = new ArrayList<SendFailedException>();
            // XXX ThreadContext threadContext = ThreadContext.get();
            // If method is not called from a web app thread, we will use the
            // first request manager to get the session
            Transport transport = ThreadContext.getEmailSession()
                    .getTransport();
            transport.connect();
            for (Email m : messages) {
                try {
                    log.info("Sending email to "
                            + Arrays.toString(m.getAllRecipients()));
                    transport.sendMessage(m, m.getAllRecipients());
                    results.add(null);
                } catch (SendFailedException e) {
                    results.add(e);
                }
            }
            transport.close();
            return results;
        } catch (NoSuchProviderException e) {
            throw new JWIGException(e);
        } catch (AuthenticationFailedException e) {
            throw new JWIGException(e);
        } catch (MessagingException e) {
            throw new JWIGException(e);
        }
    }

    /**
     * Works like {@link #sendEmails(java.util.List)} but sends the emails in a
     * separate thread. For large batches of emails this means that the
     * application does not have to wait. Errors can however not be propagated
     * using this method
     *
     * @param messages
     *            Messages to send
     * @return an empty list
     * @throws JWIGException
     */
    public List<SendFailedException> sendEmailsAsynchronously(
            final List<Email> messages) throws JWIGException {
        // XXX ThreadContext threadContext = ThreadContext.get();
        // If method is not called from a web app thread, we will use the first
        // request manager to get the session
        try {
            final Transport transport = ThreadContext.getEmailSession()
                    .getTransport();
            new Thread() {
                @Override
                public void run() {
                    try {
                        transport.connect();
                        for (Email m : messages) {
                            try {
                                log.info("Sending email to "
                                        + Arrays.toString(m.getAllRecipients()));
                                transport.sendMessage(m, m.getAllRecipients());
                            } catch (SendFailedException e) {
                                log.warn(e.getMessage(), e);
                            }
                        }
                        transport.close();
                    } catch (NoSuchProviderException e) {
                        log.error(e.getMessage(), e);
                    } catch (AuthenticationFailedException e) {
                        log.error(e.getMessage(), e);
                    } catch (MessagingException e) {
                        log.error(e.getMessage(), e);
                    }
                }

            }.start();
        } catch (NoSuchProviderException e) {
            throw new JWIGException(e);
        }
        return Collections.emptyList();
    }

    /**
     * Returns (non-null) array of cookies from the request.
     */
    public Cookie[] getCookies() {
        Cookie[] cookies = ThreadContext.get().getServletRequest().getCookies();
        if (cookies == null) {
            cookies = new Cookie[0];
        }
        return cookies;
    }

    /**
     * Adds a cookie to the response.
     *
     * @param send_now
     *            if set, the cookie is sent immediately without storing it in
     *            the response cache
     */
    protected void addCookie(Cookie c, boolean send_now) {
        if (send_now) {
            ThreadContext.get().getServletResponse().addCookie(c);
        } else {
            ThreadContext.get().getResponse().addCookie(c);
        }
    }

    /**
     * Checks whether the request was made using a secure channel, such as
     * SSL/TLS.
     */
    public boolean isSecure() {
        return ThreadContext.get().getServletRequest().isSecure();
    }

    /**
     * Gets the user for the request, using HTTP Basic authentication. Returns
     * null if not available.
     */
    public User getUser() {
        String username;
        String password;
        HttpServletRequest req = getServletRequest();
        String auth = req.getHeader("Authorization");
        if (auth == null) {
            return null;
        }
        auth = auth.trim();
        if (!auth.startsWith("Basic")) {
            return null;
        }
        auth = auth.substring(5).trim();
        try {
            auth = Base64.decodeString(auth, "ISO-8859-1");
        } catch (IllegalArgumentException e) {
            return null;
        } catch (UnsupportedEncodingException e) {
            return null;
        }
        int i = auth.indexOf(':');
        if (i == -1) {
            return null;
        }
        username = auth.substring(0, i);
        password = auth.substring(i + 1);
        return new User(username, password);
    }

    /**
     * Returns the web site object.
     */
    public final WebSite getWebSite() {
        return ThreadContext.getWebSite();
    }

    /**
     * Augments the current XML response with script code, JWIG logo, and web
     * app params. TODO: for all XML output (including XMLProducer): remove
     * empty ul/ol/... (see PostProcessor.flex in old JWIG)
     */
    @URLPattern("**")
    @Priority(MAX_PRIORITY - 1)
    @GET
    @POST
    @PUT
    @DELETE
    @TRACE
    @HEAD
    @OPTIONS
    public Object augment() {
        Object o = next();
        ThreadContext c = ThreadContext.get();
        RequestManager manager = c.getRequestManager();
        Response r = getResponse();
        if (o instanceof XML) {
            // log.debug("Augmenting: " + r.getETag());
            XML xml = (XML) o;
            String home = manager.getWebSiteURL(c.getServletRequest()
                    .isSecure());
            xml = xml
                    .prependContent(
                            "//xhtml:head",
                            XML.parseTemplate(
                                    "<script type=\"text/javascript\" src=[JWIG]></script>")
                                    .plug("JWIG", manager.makeURL("jwigJS")));
            xml = xml
                    .prependContent(
                            "//xhtml:body", //It is safe to set a constant id given that only one body may exist
                            XML.parseTemplate("<div id=\"noscript\" style=\"color:red\">" +
                                    "[ This web application uses JavaScript, which is not enabled in your browser - "
                                    + "for full functionality, use a browser that supports JavaScript ]</div>" +
                                    "<script type=\"text/javascript\"> " +
                                    "document.getElementById(\"noscript\").style.display = \"none\";" +
                                    "</script>"));
            xml = xml.appendContent("//xhtml:body",
                    XML.parseTemplate(
                            "<script type=\"text/javascript\">"
                                    + "jwig.run('<[HOME]>');" + "</script>")
                            .plug("HOME", home));
            xml = xml.appendContent("//xhtml:body",
                                    XML.parseTemplate("<script type=\"text/javascript\">"
                                    + "jwig.submitHandlerData = <[SUB]> ;" + "</script>").plug("SUB",
                                                                                               generateSubmitHandlerData(r)));
            if (auto_refresh_sessions) {
                Set<Session> sessions = r.getSessions();
                if (sessions != null && sessions.size() > 0) {
                    int minutes = 0;
                    for (Session s : sessions) {
                        int min = s.getMinutes();
                        if (minutes < min || minutes == 0) {
                            minutes = min;
                        }
                    }
                    Object[] ss = { sessions.toArray() };
                    xml = xml
                            .appendContent(
                                    "//xhtml:head",
                                    XML.parseTemplate(
                                            "<script type=\"text/javascript\">"
                                                    + "jwig.startRefreshSessions('<[REFRESH_URL]>',<[MINUTES]>);"
                                                    + "</script>")
                                            .plug("REFRESH_URL",
                                                    manager.makeURL(
                                                            "touchSessions", ss)
                                                            .toString())
                                            .plug("MINUTES", minutes - 1));
                }
            }
            if (Config.get("jwig.logo", true)) {
                xml = xml
                        .appendContent(
                                "//xhtml:body",
                                XML.parseTemplate(
                                        "<table width=\"100%\"><tr>"
                                                + "<td align=\"right\"><a href=\"http://www.brics.dk/JWIG/\">"
                                                + "<img src=[LOGOURL] alt=\"Powered by JWIG!\" style=\"border:0\"/></a></td>"
                                                + "</tr></table>").plug(
                                        "LOGOURL", home + "jwiglogo.gif"));
            }
            return xml;
        }
        return o;
    }

    private String generateSubmitHandlerData(Response r) {
        StringBuilder jSSubmitHandlerData = new StringBuilder();
        jSSubmitHandlerData.append("{");
        boolean first = true;
        for (AbstractHandler a : r.getHandlers()) {
            if (a instanceof SubmitHandler) {
                if (!first) {
                    jSSubmitHandlerData.append(",");
                } else
                    first = false;
                SubmitHandler submitHandler = (SubmitHandler) a;
                //These are the form field names that the submit handler wants to validate. Add them to the client side
                //so that this can happen
                List<String> strings = submitHandler.validatedFormFields();
                StringBuilder jSFormFieldList = new StringBuilder();
                jSFormFieldList.append("[");
                for (Iterator<String> iterator = strings.iterator(); iterator.hasNext(); ) {
                    String s = iterator.next();
                    jSFormFieldList.append("\"" + s + "\""); //No need to escape, we know that s is a valid Java identifier...
                    if (iterator.hasNext()) {
                        jSFormFieldList.append(",");
                    }
                }
                jSFormFieldList.append("]");
                jSSubmitHandlerData.append(String.format("\"%s\" : %s", a.getHandlerIdentifier(), jSFormFieldList.toString()));
            }
        }
        jSSubmitHandlerData.append("}");
        return jSSubmitHandlerData.toString();
    }

    /**
     * If a piece of XML contains a handler and this piece of XML is stored in a session
     * between requests, we need to make sure that the handler is transported
     * from one request to the next.<br/><br/>
     *
     * An example of this pattern exists in the guessing game example of jwig, where
     * XML is stored in the session and updated from one request to the next.
     *
     * This method allows for handlers to be transported from one request where the
     * handler is part of the XML and to the current request if the URL of the handler
     * is part of the XML that is returned if those two requests share the same session.
     */
    @Priority(MAX_PRIORITY - 1)
    @URLPattern("**")
    public void transportSessionHandlers() {
        next();
        ThreadContext c = ThreadContext.get();
        Set<Session> activeSessions = c.getResponse().getSessions();
        Response response = c.getResponse();
        Collection<AbstractHandler> responseHandlers = response.getHandlers();
        for (Session s : activeSessions) {
            Set<AbstractHandler> sessionHandlers = this.sessionHandlers.get(s);
            if (sessionHandlers == null) {
                sessionHandlers = new HashSet<AbstractHandler>();
                this.sessionHandlers.put(s, sessionHandlers);
            }
            for (AbstractHandler handler : sessionHandlers) {
                c.getResponse().setHandler(handler.getHandlerIdentifier(), handler);
            }
            sessionHandlers.addAll(responseHandlers);
        }
    }

    /**
     * Main web method. Catches exceptions and reports them as HTTP errors.
     */
    @URLPattern("**")
    @Priority(MAX_PRIORITY)
    @GET
    @POST
    @PUT
    @DELETE
    @TRACE
    @HEAD
    public final Object main() throws InterruptedException {
        long startTime = getTime();
        String key = Dispatcher.getClient(getServletRequest());
        AtomicInteger atomicInteger = failedRequestMap.get(key);
        WebSite website = ThreadContext.getWebSite();
        if (atomicInteger != null && atomicInteger.get() != 0) {
            int wait = atomicInteger.get();
            long millis = 1l << wait;
            if (0 < millis && millis < 10000) {
                log.info("Letting " + key + " wait for " + millis
                        + " ms before responding.");
                Thread.sleep(millis);
            } else {
                log.info("Trying to drop " + key);
                return website.sendError(
                        HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                        "Too many bad requests. You have been dropped.");
            }
        }
        try {
            log.info("Start generate response");
            Object o = next();
            if (o instanceof XML && getServletRequest().getHeader(JWIG_REGENERATE) != null) {
                Response currentResponse = getResponse();
                String ticket = ThreadContext.getHandlerCache().createTicket(currentResponse);
                getServletResponse().setHeader(JWIG_REGENERATE, ticket);
            }
            if (failedRequestMap != null) { // Redeployment can make this map null
                atomicInteger = failedRequestMap.get(key);
                if (!ThreadContext.get().isPunished() && atomicInteger != null
                        && atomicInteger.get() != 0) {
                    failedRequestMap.remove(key);
                    log.info("Forgiving " + key);
                }
            }
            long time = ((getTime() - startTime) / (long) 1E6);
            log.info("It took " + time
                    + "ms of CPU time to generate the response");
        } catch (JWIGException e) {
            log.info(e.getLogInfo());
            e.setHeaders(ThreadContext.get().getServletResponse());
            XML xml = e.getXml();
            if (xml == null) {
                return website.sendError(e.getErrorCode(), e.getMessagePage());
            } else {
                return website.sendError(e.getErrorCode(), xml, true);
            }
        } catch (Exception e) {
            log.error("Exception", e);
            return website.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                                     "Internal server error. (Details are in the server log file.)");
        }
        return null;
    }

    private long getTime() {
        long start;
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        if (bean.isCurrentThreadCpuTimeSupported()) {
            start = bean.getCurrentThreadCpuTime();
        } else {
            start = System.currentTimeMillis();
        }
        return start;
    }

    public static void punish(String key) {
        Logger.getLogger(WebContext.class).info("Punishing " + key);
        ThreadContext.get().setPunished(true);
        AtomicInteger atomicInteger;
        atomicInteger = failedRequestMap.get(key);
        if (atomicInteger == null) {
            atomicInteger = new AtomicInteger();
            failedRequestMap.put(key, atomicInteger);
        }
        atomicInteger.incrementAndGet();
    }

    private static byte[] jwigjs;
    private static String jwigjsetag = new Date().hashCode() + "";

    @Priority(MAX_PRIORITY)
    @URLPattern("jwig.js")
    public void jwigJS() throws IOException {
        String etag = getServletRequest().getHeader("If-None-Match");
        if (jwigjsetag.equals(etag)) {
            getServletResponse().setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            getServletResponse().getOutputStream().close();
            return;
        }
        if (jwigjs == null) {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            try (InputStream stream = WebContext.class.getResourceAsStream("jwig.js")) {
                byte[] buf = new byte[1024];
                int read;
                while ((read = stream.read(buf)) != -1) {
                    bout.write(buf, 0, read);
                }
            }
            jwigjs = bout.toByteArray();
        }
        getResponse().setContentType("text/javascript");
        getServletResponse().setHeader("ETag", jwigjsetag);
        ServletOutputStream outputStream = getServletResponse().getOutputStream();
        outputStream.write(jwigjs);
        outputStream.close();
    }

    /**
     * Server cache web method. Checks whether the client's cached resource is
     * valid (using <code>If-Modified-Since</code> and <code>ETag</code>) and
     * whether the requested resource is in the server cache. Only considers GET
     * requests. Invokes {@link #next()} if a valid cached resource is not
     * available.
     */
    @GET
    @URLPattern("**")
    @Priority(CACHE_PRIORITY)
    public Object cache() {
        try {
            ThreadContext.getDependencyMap().beginTransaction(true);
            ThreadContext c = ThreadContext.get();
            HttpServletRequest request = c.getServletRequest();
            HttpServletResponse response = c.getServletResponse();
            Cache cache = ThreadContext.getCache();
            String cacheURL = c.getRequestURL();
            Response cachedResponse = cache.get(cacheURL);
            if (cachedResponse == null) {
                String augmentationString = "<|>"
                        + WebApp.get().getWebSite()
                                .getCacheAugmentationString();
                cacheURL = c.getRequestURL() + augmentationString;
                cachedResponse = cache.get(cacheURL);
            }
            if (cachedResponse != null) {
                String cacheControl = request.getHeader("cache-control");
                long if_modified_since = -1;
                try {
                    if_modified_since = request
                            .getDateHeader("If-Modified-Since");
                } catch (IllegalArgumentException e) {
                    log.info("Client sent invalid If-Modified-Since");
                }
                try {
                    String if_none_match = request.getHeader("If-None-Match");
                    long lastModified = (cachedResponse.getLastModified() / 1000) * 1000;
                    long ifModifiedSince = (if_modified_since / 1000) * 1000;
                    if (if_none_match == null && if_modified_since == -1
                            && cacheControl != null
                            && cacheControl.equals("max-age=0")) {
                        log.info("Client refreshed the page. Removing the old one from the cache");
                        cache.remove(cacheURL);

                    } else {
                        if ((if_none_match != null && if_none_match.equals("\""
                                + cachedResponse.getETag() + "\""))
                                || (if_none_match == null
                                        && if_modified_since != -1 && lastModified <= ifModifiedSince)) {
                            response.setStatus(304);
                            response.flushBuffer();
                            log.info("Client cache valid");
                            return null;
                        }
                        log.info("Response in server cache");
                        ThreadContext.get().setResponse(cachedResponse);
                        return cachedResponse.getObject();
                    }
                } catch (IOException e) {
                    log.info("IOException in serverCache", e);
                    if (response.isCommitted()) {
                        return null;
                    }
                }
            }
            Object result = next();
            if (result instanceof XML || result instanceof String) {
                Response currentResponse = ThreadContext.get().getResponse();
                currentResponse.setResult(result);
                // This may also remove pages from the cache
                boolean shouldCache = ThreadContext.getDependencyMap().mergeTransaction();
                if (shouldCache && currentResponse.getStatus() == HttpServletResponse.SC_OK) {
                    String url = c.getRequestURL();
                    url = getCacheAugmentedString(url);
                    Response newResponseForCache = new Response(currentResponse);
                    newResponseForCache.setResult(currentResponse.getResult());
                    ThreadContext.getCache().put(url, newResponseForCache);
                }
            }
            return result;
        } finally {
            ThreadContext.getDependencyMap().removeTransaction();
        }
    }

    protected String getCacheAugmentedString(String url) {
        boolean cacheAugmented = ThreadContext.get().isCacheAugmented();
        if (cacheAugmented) {
            url += "<|>" + getWebSite().getCacheAugmentationString();
        }
        return url;
    }

    /**
     * Invalidates pages in the cache based on which objects are updated
     */
    @PUT
    @DELETE
    @POST
    @URLPattern("**")
    @Priority(CACHE_PRIORITY)
    public void handleUpdatedObjects() {
        ThreadContext.getDependencyMap().beginTransaction(false);
        try {
            next();
            ThreadContext.getDependencyMap().mergeTransaction();
        } finally {
            ThreadContext.getDependencyMap().removeTransaction();
        }
    }

    /**
     * Invoked automatically from generated XML pages for refreshing active
     * sessions.
     */
    @Priority(PRE_CACHE)
    public String touchSessions(Session[] s) {
        // just preparing the session objects is enough to refresh them
        return "";
    }

    /**
     * Invoked automatically from generated XML pages for executing a handler.
     */
    @Priority(HANDLERS)
    @URLPattern("handlers/*")
    public Object handlers() throws IOException {
        ThreadContext c = ThreadContext.get();
        String referer = getRequestReferer(c.getServletRequest());
        Response referer_response = findOrRegenerateRefererResponse(referer);
        String requestURL = c.getRequestURL();
        if (requestURL.endsWith("-validate")) {
            requestURL = requestURL.substring(0, requestURL.length() - "-validate".length());
            AbstractHandler handler = referer_response.getHandler(requestURL);
            if (handler instanceof SubmitHandler) {
                SubmitHandler submitHandler = (SubmitHandler) handler;
                return submitHandler.validate();
            } else {
                return null;
            }
        }
        AbstractHandler handler = referer_response.getHandler(requestURL);

        if (handler == null) {
            throw new BadRequestException("Handler not found: " + c.getRequestURL());
        } else {
            //Transport all sessions from referer. This is useful because people may use
            //sessions declared in the outer method of an anonymous handler. This pattern
            //exists for example in the GuessingGame example
            for (Session s : referer_response.getSessions()) {
                getResponse().addSession(s);
            }
            if (handler instanceof SubmitHandler) {
                SubmitHandler submitHandler = (SubmitHandler) handler;
                Object o = submitHandler.validate();
                if (o != null) {
                    if (o instanceof XML) {
                        o = ((XML) o).getString();
                    }
                    throw new BadRequestException(o.toString());
                }
            }
            Object o = handler.process(referer);
            if (o == null) {
                return new URL(referer);
            }
            return o;
        }
    }

    private Response findOrRegenerateRefererResponse(String referer) throws IOException {
        ThreadContext c = ThreadContext.get();
        HttpServletRequest request = c.getServletRequest();
        HttpServletResponse response = c.getServletResponse();
        Response referer_response = getReferer(referer);
        //If the referer response does not exist, then we try to reconstruct it
        if (referer_response == null || referer_response.getHandler(c.getRequestURL()) == null) {
            log.info("Handler could not be found in the cache. Referer is: " + referer);
            HttpURLConnection con = (HttpURLConnection) new URL(referer)
                    .openConnection();
            final String authorization = "Authorization";
            String auth = request.getHeader(authorization);
            if (auth != null) {
                con.addRequestProperty(authorization, auth);
            }
            String cookie = "cookie";
            String cook = request.getHeader(cookie);
            if (cook != null) {
                con.addRequestProperty(cookie, cook);
            }
            con.addRequestProperty(JWIG_REGENERATE, "Regenerate");
            con.setConnectTimeout(10000);
            con.setReadTimeout(50000);
            con.setUseCaches(false);
            int code = con.getResponseCode();
            con.disconnect();
            if (code != HttpServletResponse.SC_OK) {
                if (code == HttpServletResponse.SC_GONE) {
                    String defunctSessionId = con
                            .getHeaderField(JWIG_DEFUNCT_SESSION);
                    if (defunctSessionId != null) {
                        throw new SessionDefunctException(defunctSessionId);
                    }
                }
                log.error("Referer response not successfully regenerated, response code "
                        + code);
                response.setHeader("Retry-After", "600");
                throw new ServerBusyException("Service unavailable. (Error "
                                        + code
                                        + " while attempting to regenerate referer response.)");
            }
            String regenerate_id = con.getHeaderField(JWIG_REGENERATE);
                HandlerCache handlerCache = ThreadContext.getHandlerCache();
            if (handlerCache.hasResponse(regenerate_id)) {
                referer_response = handlerCache.removeResponse(regenerate_id);
            } else {
                //There has been a request in between, so the handler is in the ordinary cache
                referer_response = getReferer(referer);
            }
            if (referer_response == null) { //This should never happen
                throw new JWIGException("Failed to regenerate and retrieve referer response.");
            }
        }
        return referer_response;
    }

    private boolean hasReferer(HttpServletRequest request) {
        return request.getHeader("Referer") != null;
    }

    private String getRequestReferer(HttpServletRequest request) {
        // TODO: can we rely on Referer? (if not, encode the relevant parts in the submit URL)
        String referer = request.getHeader("Referer");
        if (referer == null) {
            throw new BadRequestException("Referer missing.");
        }
        if (!referer.startsWith("http:") && !referer.startsWith("https:")) {
            throw new BadRequestException("Referer protocol invalid");
        }
        log.info("Referer: " + referer);
        return referer;
    }

    private Response getReferer(String referer) {
        Cache cache = ThreadContext.getCache();
        Response referer_response = cache.get(referer);
        if (referer_response == null) {
            referer_response = cache.get(referer + "<|>"
                    + getWebSite().getCacheAugmentationString());
        }
        return referer_response;
    }

    /**
     * Invokes the next web method in the chain.
     */
    public static Object next() {
        ThreadContext.getDispatcher().invokeNextWebMethod();
        ThreadContext threadContext = ThreadContext.get();
        RuntimeException throwable = threadContext.getThrowable();
        if (throwable != null) {
            threadContext.setThrowable(null);
            throw throwable;
        }
        return threadContext.getCurrentResult();
    }

    /**
     * Returns the web app parameter of the given name.
     */
    @SuppressWarnings("unchecked")
    public <E> E getWebAppParam(String name, Class<E> type) {
        ThreadContext threadContext = ThreadContext.get();
        String objects = threadContext.getWebAppParams().get(name);
        return (E) threadContext.getRequestManager().deserializeArgument(
                new Object[] { objects }, type, name, false, false, null);
    }

    /**
     * Gets the string representation of the web app parameter with this name
     */
    protected String getWebAppParam(String name) {
        return getWebAppParam(name, String.class);
    }

    /*
     * @URLPattern("**")
     * @Priority(MAX_PRIORITY) public XML addSecurityTokens() { Object result =
     * next(); if (result instanceof XML) { XML xml = (XML) result;
     * MessageDigest md; try { md = MessageDigest.getInstance("SHA-1"); } catch
     * (NoSuchAlgorithmException e) { throw new JWIGException(e); } xml =
     * xml.plug("SECUREFORM", ""); NodeList<Element> forms =
     * xml.getElements("//xhtml:form"); xml = xml.gapify("//xhtml:form",
     * "SECUREFORM"); LinkedList<XML> securedForms = new LinkedList<XML>(); for
     * (XML form : forms) { md.reset(); StringBuffer secureValues = new
     * StringBuffer(); for (XML hiddenInput :
     * form.getElements("//xhtml:input[@type='hidden']")) {
     * secureValues.append(Base64.encodeString(hiddenInput.getString("@name")))
     * .append(":");
     * secureValues.append(Base64.encodeString(hiddenInput.getString("@value")))
     * .append(";"); }
     * secureValues.append(getWebSite().getCacheAugmentationString()); String
     * macString =
     * String.valueOf(Base64.encode(md.digest(secureValues.toString()
     * .getBytes()))); String mac =
     * String.valueOf(Base64.encode(md.digest(macString.getBytes()))); XML
     * securityToken =
     * XML.parseTemplate("<input type='hidden' name='securityToken' value=[VAL] />"
     * ).plug("VAL", mac); form = form.appendContent(securityToken);
     * securedForms.add(form); } xml = xml.plugList("SECUREFORM", securedForms);
     * return xml; } return null; }
     */
TOP

Related Classes of dk.brics.jwig.WebContext

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.