PluginResource.java

362 lines | 16.248 kB Blame History Raw Download
/*
 * Copyright 2010-2013 Ning, Inc.
 * Copyright 2014-2015 Groupon, Inc
 * Copyright 2014-2015 The Billing Project, LLC
 *
 * The Billing Project licenses this file to you under the Apache License, version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package org.killbill.billing.jaxrs.resources;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.servlet.ReadListener;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import org.killbill.billing.account.api.AccountUserApi;
import org.killbill.billing.jaxrs.util.Context;
import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.util.api.AuditUserApi;
import org.killbill.billing.util.api.CustomFieldUserApi;
import org.killbill.billing.util.api.TagUserApi;
import org.killbill.clock.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.codahale.metrics.annotation.Timed;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.io.ByteStreams;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.sun.jersey.api.representation.Form;
import com.wordnik.swagger.annotations.Api;

@Singleton
@Path(JaxrsResource.PLUGINS_PATH + "{subResources:.*}")
@Api(value = JaxrsResource.PLUGINS_PATH + "{subResources:.*}", description = "Plugins servlets", hidden = true)
public class PluginResource extends JaxRsResourceBase {

    private static final Logger log = LoggerFactory.getLogger(PluginResource.class);
    private static final String UTF_8_STRING = "UTF-8";
    private static final Charset UTF_8 = Charset.forName(UTF_8_STRING);

    private final HttpServlet osgiServlet;

    @Inject
    public PluginResource(@Named("osgi") final HttpServlet osgiServlet, // See DefaultOSGIModule.OSGI_NAMED
                          final JaxrsUriBuilder uriBuilder,
                          final TagUserApi tagUserApi,
                          final CustomFieldUserApi customFieldUserApi,
                          final AuditUserApi auditUserApi,
                          final AccountUserApi accountUserApi,
                          final PaymentApi paymentApi,
                          final Clock clock,
                          final Context context) {
        super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, paymentApi, clock, context);
        this.osgiServlet = osgiServlet;
    }

    @Timed
    @DELETE
    public Response doDELETE(@javax.ws.rs.core.Context final HttpServletRequest request,
                             @javax.ws.rs.core.Context final HttpServletResponse response,
                             @javax.ws.rs.core.Context final ServletContext servletContext,
                             @javax.ws.rs.core.Context final ServletConfig servletConfig,
                             @javax.ws.rs.core.Context final UriInfo uriInfo) throws ServletException, IOException {
        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig, uriInfo);
    }

    @Timed
    @GET
    public Response doGET(@javax.ws.rs.core.Context final HttpServletRequest request,
                          @javax.ws.rs.core.Context final HttpServletResponse response,
                          @javax.ws.rs.core.Context final ServletContext servletContext,
                          @javax.ws.rs.core.Context final ServletConfig servletConfig,
                          @javax.ws.rs.core.Context final UriInfo uriInfo) throws ServletException, IOException {
        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig, uriInfo);
    }

    @Timed
    @OPTIONS
    public Response doOPTIONS(@javax.ws.rs.core.Context final HttpServletRequest request,
                              @javax.ws.rs.core.Context final HttpServletResponse response,
                              @javax.ws.rs.core.Context final ServletContext servletContext,
                              @javax.ws.rs.core.Context final ServletConfig servletConfig,
                              @javax.ws.rs.core.Context final UriInfo uriInfo) throws ServletException, IOException {
        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig, uriInfo);
    }

    @Timed
    @POST
    @Consumes("application/x-www-form-urlencoded")
    public Response doFormPOST(final MultivaluedMap<String, String> form,
                               @javax.ws.rs.core.Context final HttpServletRequest request,
                               @javax.ws.rs.core.Context final HttpServletResponse response,
                               @javax.ws.rs.core.Context final ServletContext servletContext,
                               @javax.ws.rs.core.Context final ServletConfig servletConfig,
                               @javax.ws.rs.core.Context final UriInfo uriInfo) throws ServletException, IOException {
        return serviceViaOSGIPlugin(form, request, response, servletContext, servletConfig, uriInfo);
    }

    @Timed
    @POST
    public Response doPOST(@javax.ws.rs.core.Context final HttpServletRequest request,
                           @javax.ws.rs.core.Context final HttpServletResponse response,
                           @javax.ws.rs.core.Context final ServletContext servletContext,
                           @javax.ws.rs.core.Context final ServletConfig servletConfig,
                           @javax.ws.rs.core.Context final UriInfo uriInfo) throws ServletException, IOException {
        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig, uriInfo);
    }

    @Timed
    @PUT
    public Response doPUT(@javax.ws.rs.core.Context final HttpServletRequest request,
                          @javax.ws.rs.core.Context final HttpServletResponse response,
                          @javax.ws.rs.core.Context final ServletContext servletContext,
                          @javax.ws.rs.core.Context final ServletConfig servletConfig,
                          @javax.ws.rs.core.Context final UriInfo uriInfo) throws ServletException, IOException {
        return serviceViaOSGIPlugin(request, response, servletContext, servletConfig, uriInfo);
    }

    @Timed
    @HEAD
    public Response doHEAD(@javax.ws.rs.core.Context final HttpServletRequest request,
                           @javax.ws.rs.core.Context final HttpServletResponse response,
                           @javax.ws.rs.core.Context final ServletContext servletContext,
                           @javax.ws.rs.core.Context final ServletConfig servletConfig,
                           @javax.ws.rs.core.Context final UriInfo uriInfo) throws ServletException, IOException {
        serviceViaOSGIPlugin(request, response, servletContext, servletConfig, uriInfo);

        // Make sure to return 204
        return Response.noContent().build();
    }

    private Response serviceViaOSGIPlugin(final HttpServletRequest request, final HttpServletResponse response,
                                          final ServletContext servletContext, final ServletConfig servletConfig,
                                          final UriInfo uriInfo) throws ServletException, IOException {
        return serviceViaOSGIPlugin(request, request.getInputStream(), new Form(), response, servletContext, servletConfig, uriInfo);
    }

    private Response serviceViaOSGIPlugin(final MultivaluedMap<String, String> form,
                                          final HttpServletRequest request, final HttpServletResponse response,
                                          final ServletContext servletContext, final ServletConfig servletConfig,
                                          final UriInfo uriInfo) throws ServletException, IOException {
        // form will contain form parameters, if any. Even if the request contains such parameters, it may be empty
        // if a filter (e.g. Shiro) has already consumed them (see kludge below)
        return serviceViaOSGIPlugin(request, createInputStream(request, form), form, response, servletContext, servletConfig, uriInfo);
    }

    private Response serviceViaOSGIPlugin(final HttpServletRequest request, final InputStream inputStream, final MultivaluedMap<String, String> formData,
                                          final HttpServletResponse response, final ServletContext servletContext,
                                          final ServletConfig servletConfig, final UriInfo uriInfo) throws ServletException, IOException {
        prepareOSGIRequest(request, servletContext, servletConfig);
        osgiServlet.service(new OSGIServletRequestWrapper(request, inputStream, formData, uriInfo.getQueryParameters()), new OSGIServletResponseWrapper(response));

        if (response.isCommitted()) {
            if (response.getStatus() >= 400) {
                log.warn("{} responded {}", request.getPathInfo(), response.getStatus());
            }
            // Jersey will want to return 204, but the servlet should have done the right thing already
            return null;
        } else {
            return Response.status(response.getStatus()).build();
        }
    }

    private InputStream createInputStream(final HttpServletRequest request, final MultivaluedMap<String, String> form) throws IOException {
        // /!\ Kludge alert (pierre) /!\
        // This is awful... But because of various servlet filters we have in place, include Shiro,
        // the request parameters and/or body at this point have already been looked at.
        // We can't use @FormParam in PluginResource because we don't know the form parameter names
        // in advance.
        // So... We just stick them back in :-)
        // TODO Support application/x-www-form-urlencoded vs multipart/form-data
        final ByteArrayOutputStream out = new ByteArrayOutputStream();

        final Map<String, String> data = new HashMap<String, String>();
        for (final String key : request.getParameterMap().keySet()) {
            data.put(key, request.getParameter(key));
        }
        for (final String key : form.keySet()) {
            data.put(key, form.getFirst(key));
        }
        appendFormParametersToBody(out, data);

        ByteStreams.copy(request.getInputStream(), out);
        return new ByteArrayInputStream(out.toByteArray());
    }

    private void appendFormParametersToBody(final ByteArrayOutputStream out, final Map<String, String> data) throws IOException {
        int idx = 0;
        for (final String key : data.keySet()) {
            if (idx > 0) {
                out.write("&".getBytes(UTF_8));
            }

            out.write((key + "=" + URLEncoder.encode(data.get(key), UTF_8_STRING)).getBytes(UTF_8));
            idx++;
        }
    }

    private void prepareOSGIRequest(final HttpServletRequest request, final ServletContext servletContext, final ServletConfig servletConfig) {
        request.setAttribute("killbill.osgi.servletContext", servletContext);
        request.setAttribute("killbill.osgi.servletConfig", servletConfig);
    }

    // Request wrapper to hide the /plugins prefix to OSGI bundles and fiddle with the input stream
    private static final class OSGIServletRequestWrapper extends HttpServletRequestWrapper {

        private final InputStream inputStream;
        private final Map<String, String[]> parameterMap;

        public OSGIServletRequestWrapper(final HttpServletRequest request, final InputStream inputStream, final MultivaluedMap<String, String> formData, final MultivaluedMap<String, String> queryParameters) {
            super(request);
            this.inputStream = inputStream;
            this.parameterMap = new HashMap<String, String[]>();

            // Query string parameters and posted form data must appear in the parameters
            final LinkedHashMultimap<String, String> tmpParameterMap = LinkedHashMultimap.<String, String>create();
            for (final String formDataKey : formData.keySet()) {
                tmpParameterMap.putAll(formDataKey, formData.get(formDataKey));
            }
            for (final String queryParameterKey : queryParameters.keySet()) {
                tmpParameterMap.putAll(queryParameterKey, queryParameters.get(queryParameterKey));
            }
            for (final String parameterKey : request.getParameterMap().keySet()) {
                tmpParameterMap.putAll(parameterKey, ImmutableList.<String>copyOf(request.getParameterMap().get(parameterKey)));
            }
            for (final String value : tmpParameterMap.keys()) {
                parameterMap.put(value, tmpParameterMap.get(value).toArray(new String[0]));
            }
        }

        @Override
        public String getParameter(final String name) {
            final String[] values = parameterMap.get(name);
            if (values == null || values.length == 0) {
                return null;
            } else {
                return values[0];
            }
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            return Collections.<String, String[]>unmodifiableMap(parameterMap);
        }

        @Override
        public Enumeration<String> getParameterNames() {
            return Collections.<String>enumeration(parameterMap.keySet());
        }

        @Override
        public String[] getParameterValues(final String name) {
            return parameterMap.get(name);
        }

        @Override
        public String getPathInfo() {
            return super.getPathInfo().replace(JaxrsResource.PLUGINS_PATH, "");
        }

        @Override
        public String getContextPath() {
            return JaxrsResource.PLUGINS_PATH;
        }

        @Override
        public String getServletPath() {
            return super.getServletPath().replace(JaxrsResource.PLUGINS_PATH, "");
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            return new ServletInputStreamWrapper(inputStream);
        }
    }

    private static final class ServletInputStreamWrapper extends ServletInputStream {

        private final InputStream inputStream;
        private final AtomicBoolean eof = new AtomicBoolean(false);

        public ServletInputStreamWrapper(final InputStream inputStream) {
            this.inputStream = inputStream;
        }

        @Override
        public int read() throws IOException {
            final int next = inputStream.read();
            if (next == -1) {
                eof.set(true);
            }
            return next;
        }

        @Override
        public boolean isFinished() {
            return eof.get();
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(final ReadListener readListener) {
            throw new UnsupportedOperationException("setReadListener");
        }
    }

    private static final class OSGIServletResponseWrapper extends HttpServletResponseWrapper {

        public OSGIServletResponseWrapper(final HttpServletResponse response) {
            super(response);
        }
    }
}