Jasig/WebproxyPortlet

View on GitHub
src/main/java/org/jasig/portlet/proxy/mvc/portlet/proxy/ProxyPortletController.java

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *
 *   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.jasig.portlet.proxy.mvc.portlet.proxy;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

import javax.annotation.Resource;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.EventRequest;
import javax.portlet.EventResponse;
import javax.portlet.PortletPreferences;
import javax.portlet.PortletRequest;
import javax.portlet.PortletSession;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.ResourceRequest;
import javax.portlet.ResourceResponse;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apereo.portal.search.SearchConstants;
import org.apereo.portal.search.SearchResults;
import org.jasig.portlet.proxy.search.ISearchService;
import org.jasig.portlet.proxy.service.IContentRequest;
import org.jasig.portlet.proxy.service.IContentResponse;
import org.jasig.portlet.proxy.service.IContentService;
import org.jasig.portlet.proxy.service.proxy.document.IDocumentFilter;
import org.jasig.portlet.proxy.service.proxy.document.URLRewritingFilter;
import org.jasig.portlet.proxy.service.web.HttpContentResponseImpl;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.portlet.bind.annotation.ActionMapping;
import org.springframework.web.portlet.bind.annotation.EventMapping;
import org.springframework.web.portlet.bind.annotation.RenderMapping;
import org.springframework.web.portlet.bind.annotation.ResourceMapping;

/**
 * ProxyPortletController is the main view controller for web proxy portlets.
 *
 * @author Jen Bourey, jennifer.bourey@gmail.com
 * @version $Id: $Id
 */
@Controller
@RequestMapping("VIEW")
public class ProxyPortletController {

    /** Constant <code>PREF_CHARACTER_ENCODING="sourcePageCharacterEncoding"</code> */
    public static final String PREF_CHARACTER_ENCODING = "sourcePageCharacterEncoding";
    /** Constant <code>CHARACTER_ENCODING_DEFAULT="UTF-8"</code> */
    public static final String CHARACTER_ENCODING_DEFAULT = "UTF-8";
    /** Constant <code>CONTENT_SERVICE_KEY="contentService"</code> */
    protected static final String CONTENT_SERVICE_KEY = "contentService";
    /** Constant <code>FILTER_LIST_KEY="filters"</code> */
    protected static final String FILTER_LIST_KEY = "filters";
    protected final Logger log = LoggerFactory.getLogger(this.getClass());
    private static final String PROXY_RESPONSE_KEY = "proxyResponse";
    @Autowired
    private ApplicationContext applicationContext;
    private final List<Pattern> knownHtmlContentTypes = new ArrayList<Pattern>();
    @Resource(name = "contentSearchProvider")
    private ISearchService searchService;

    /**
     * <p>Setter for the field <code>knownHtmlContentTypes</code>.</p>
     *
     * @param contentTypes a {@link java.util.List} object
     */
    @Required
    @Resource(name = "knownHtmlContentTypes")
    public void setKnownHtmlContentTypes(List<String> contentTypes) {
        knownHtmlContentTypes.clear();
        for (String contentType : contentTypes) {
            knownHtmlContentTypes.add(Pattern.compile(contentType));
        }
    }

    /**
     * <p>showContent.</p>
     *
     * @param request a {@link javax.portlet.RenderRequest} object
     * @param response a {@link javax.portlet.RenderResponse} object
     */
    @RenderMapping
    public void showContent(final RenderRequest request, final RenderResponse response) {
        log.debug("Entering render mapping");
        IContentResponse proxyResponse = null;
        try {
            // From action phase?
            proxyResponse = (IContentResponse) request.getPortletSession().getAttribute(PROXY_RESPONSE_KEY);
            if (proxyResponse == null) {
                // retrieve the HTML content
                log.debug("proxyResponse not found in request attribute proxyResponse -- invoking proxy method");
                proxyResponse = invokeProxy(request);
            } else {
                log.debug("proxyResponse found(!) in request attribute proxyResponse -- using it on this render and removing it from session");
                request.getPortletSession().removeAttribute(PROXY_RESPONSE_KEY);
            }
            final List<IDocumentFilter> filters = prepareFilters(request);
            final Document document = parseDocument(request, proxyResponse);

            // apply each of the document filters in order
            for (final IDocumentFilter filter : filters) {
                filter.filter(document, proxyResponse, request, response);
            }

            // write out the final content
            OutputStream out = null;
            try {
                out = response.getPortletOutputStream();
                IOUtils.write(document.html(), out);
                out.flush();
            } catch (IOException e) {
                log.error("Exception writing proxy content", e);
            } finally {
                IOUtils.closeQuietly(out);
            }

        } catch (IOException e) {
            log.error("Error parsing HTML content", e);
        } finally {
            if (proxyResponse != null) {
                proxyResponse.close();
            }
        }

    }

    /**
     * <p>proxyTarget.</p>
     *
     * @param url a {@link java.lang.String} object
     * @param request a {@link javax.portlet.ActionRequest} object
     * @param response a {@link javax.portlet.ActionResponse} object
     * @throws java.io.IOException if any.
     */
    @ActionMapping
    public void proxyTarget(final @RequestParam("proxy.url") String url, final ActionRequest request,
                            final ActionResponse response) throws IOException {
        log.debug("Entering action mapping");
        IContentResponse proxyResponse = null;
        try {
            proxyResponse = invokeProxy(request);
            assert proxyResponse != null;
            request.getPortletSession().setAttribute(PROXY_RESPONSE_KEY, proxyResponse);

            // TODO: this probably can only be an HTTP content type
            if (proxyResponse instanceof HttpContentResponseImpl) {

                // Determine the content type of the proxied response.  If this is
                // not an HTML type, we need to construct a resource URL instead
                final HttpContentResponseImpl httpContentResponse = (HttpContentResponseImpl) proxyResponse;
                final String responseContentType = httpContentResponse.getHeaders().get("Content-Type");
                log.debug("content-type: {}", responseContentType);
                for (Pattern contentType : knownHtmlContentTypes) {
                    if (responseContentType != null && contentType.matcher(responseContentType).matches()) {
                        final Map<String, String[]> params = request.getParameterMap();
                        response.setRenderParameters(params);
                        log.debug("found an HTML content type match, so leaving action mapping now");
                        return;
                    }
                }

            }

            // if this is not an HTML content type, use the corresponding resource
            // URL in the session
            log.warn("We should not reach this code unless the TODO note is wrong");
            final PortletSession session = request.getPortletSession();
            @SuppressWarnings("unchecked") final ConcurrentMap<String, String> rewrittenUrls = (ConcurrentMap<String, String>) session.getAttribute(URLRewritingFilter.REWRITTEN_URLS_KEY);
            response.sendRedirect(rewrittenUrls.get(url));
        } finally {
            // closed in Render phase
            /*
            if (proxyResponse != null) {
                proxyResponse.close();
            }
             */
        }
    }

    /**
     * <p>proxyResourceTarget.</p>
     *
     * @param url a {@link java.lang.String} object
     * @param request a {@link javax.portlet.ResourceRequest} object
     * @param response a {@link javax.portlet.ResourceResponse} object
     */
    @ResourceMapping
    public void proxyResourceTarget(final @RequestParam("proxy.url") String url, final ResourceRequest request, final ResourceResponse response) {
        log.debug("Entering resource mapping");
        IContentResponse proxyResponse = null;
        OutputStream out = null;

        try {
            proxyResponse = invokeProxy(request);

            // TODO: find a cleaner way to handle this.  we probably can't ever
            // have anything except an HTTP response
            if (proxyResponse instanceof HttpContentResponseImpl) {
                // replay any response headers from the proxied target
                HttpContentResponseImpl httpProxyResponse = (HttpContentResponseImpl) proxyResponse;
                for (Map.Entry<String, String> header : httpProxyResponse.getHeaders().entrySet()) {
                    response.setProperty(header.getKey(), header.getValue());
                }
            }

            // write out all proxied content
            out = response.getPortletOutputStream();
            IOUtils.copyLarge(proxyResponse.getContent(), out);
            out.flush();

        } catch (IOException e) {
            response.setProperty(ResourceResponse.HTTP_STATUS_CODE, String.valueOf(HttpServletResponse.SC_UNAUTHORIZED));
            log.error("Exception writing proxied content", e);
        } finally {
            if (proxyResponse != null) {
                proxyResponse.close();
            }
            IOUtils.closeQuietly(out);
        }

    }

    /**
     * <p>searchRequest.</p>
     *
     * @param request a {@link javax.portlet.EventRequest} object
     * @param response a {@link javax.portlet.EventResponse} object
     */
    @EventMapping(SearchConstants.SEARCH_REQUEST_QNAME_STRING)
    public void searchRequest(EventRequest request, EventResponse response) {
        log.debug("EVENT HANDLER -- START");

        try {
            // retrieve the HTML content
            final IContentResponse proxyResponse = invokeProxy(request);
            final Document document = parseDocument(request, proxyResponse);
            SearchResults searchResults = searchService.search(request, document);
            response.setEvent(SearchConstants.SEARCH_RESULTS_QNAME, searchResults);
        } catch (IOException e) {
            throw new RuntimeException("Search request failed", e);
        }

        log.debug("EVENT HANDLER -- END");
    }

    /*
     * Implementation (private stuff)
     */

    /**
     * @throws NoSuchBeanDefinitionException If there is no such bean
     */
    private IContentService<IContentRequest, IContentResponse> selectContentService(final PortletRequest req) {
        final PortletPreferences prefs = req.getPreferences();
        final String contentServiceKey = prefs.getValue(CONTENT_SERVICE_KEY, null);
        @SuppressWarnings("unchecked") final IContentService<IContentRequest, IContentResponse> rslt = applicationContext.getBean(contentServiceKey, IContentService.class);
        return rslt;
    }

    private IContentResponse invokeProxy(final PortletRequest req) {
        log.debug("Entering invokeProxy()");
        // locate the content service to use to retrieve our HTML content
        final IContentService<IContentRequest, IContentResponse> contentService = selectContentService(req);

        final IContentRequest proxyRequest;
        try {
            proxyRequest = contentService.getRequest(req);
        } catch (Exception e) {
            throw new RuntimeException("URL was not in the proxy list", e);
        }

        // retrieve the HTML content
        final IContentResponse rslt;
        try {
            rslt = contentService.getContent(proxyRequest, req);
        } catch (Exception e) {
            throw new RuntimeException("Failed to proxy content", e);
        }

        log.debug("Leaving invokeProxy()");
        return rslt;
    }

    private List<IDocumentFilter> prepareFilters(final PortletRequest req) {
        final PortletPreferences preferences = req.getPreferences();
        final List<IDocumentFilter> filters = new ArrayList<IDocumentFilter>();
        final String[] filterKeys = preferences.getValues(FILTER_LIST_KEY, new String[]{});
        for (final String filterKey : filterKeys) {
            final IDocumentFilter filter = applicationContext.getBean(filterKey, IDocumentFilter.class);
            filters.add(filter);
        }
        return filters;
    }

    private Document parseDocument(final PortletRequest req, final IContentResponse proxyResponse) throws IOException {
        log.debug("Entering parseDocument()");
        final PortletPreferences preferences = req.getPreferences();
        String sourceEncodingFormat = preferences.getValue(PREF_CHARACTER_ENCODING, CHARACTER_ENCODING_DEFAULT);
        log.debug("encoding format: {}", sourceEncodingFormat);
        log.debug("proxyResponse content: {}", proxyResponse.getContent().toString());
        log.debug("proxyResponse location: {}", proxyResponse.getProxiedLocation());
        final Document rslt = Jsoup.parse(proxyResponse.getContent(), sourceEncodingFormat,
                proxyResponse.getProxiedLocation());
        log.debug("Leaving parseDocument()");
        return rslt;
    }

}