au.edu.anu.portal.portlets.sakaiconnector.PortletDispatcher.java Source code

Java tutorial

Introduction

Here is the source code for au.edu.anu.portal.portlets.sakaiconnector.PortletDispatcher.java

Source

/**
 * Copyright 2009-2012 The Australian National University
 *
 *    Licensed 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 au.edu.anu.portal.portlets.sakaiconnector;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.GenericPortlet;
import javax.portlet.PortletConfig;
import javax.portlet.PortletException;
import javax.portlet.PortletMode;
import javax.portlet.PortletModeException;
import javax.portlet.PortletPreferences;
import javax.portlet.PortletRequest;
import javax.portlet.PortletRequestDispatcher;
import javax.portlet.PortletURL;
import javax.portlet.ReadOnlyException;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.ValidatorException;

import lombok.extern.apachecommons.CommonsLog;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;

import au.edu.anu.portal.portlets.sakaiconnector.helper.SakaiWebServiceHelper;
import au.edu.anu.portal.portlets.sakaiconnector.logic.SakaiWebServiceLogic;
import au.edu.anu.portal.portlets.sakaiconnector.models.Site;
import au.edu.anu.portal.portlets.sakaiconnector.support.CollectionsSupport;
import au.edu.anu.portal.portlets.sakaiconnector.support.HttpSupport;
import au.edu.anu.portal.portlets.sakaiconnector.support.OAuthSupport;
import au.edu.anu.portal.portlets.sakaiconnector.utils.Constants;
import au.edu.anu.portal.portlets.sakaiconnector.utils.Messages;

@CommonsLog
public class PortletDispatcher extends GenericPortlet {

    // pages
    private String viewUrl;
    private String editUrl;
    private String proxyUrl;
    private String errorUrl;
    private String configUrl;

    // params
    private String key;
    private String secret;
    private String endpoint;
    private String adminUsername;
    private String adminPassword;
    private String loginUrl;
    private String scriptUrl;
    private String allowedTools;

    //attribute mappings
    private String attributeMappingForUsername;

    // local
    private boolean replayForm;
    private boolean isValid;

    //caches
    private Cache cache;

    public void init(PortletConfig config) throws PortletException {
        super.init(config);
        log.info("init()");

        //get pages
        viewUrl = config.getInitParameter("viewUrl");
        editUrl = config.getInitParameter("editUrl");
        proxyUrl = config.getInitParameter("proxyUrl");
        errorUrl = config.getInitParameter("errorUrl");
        configUrl = config.getInitParameter("configUrl");

        //get config params
        attributeMappingForUsername = config.getInitParameter("portal.attribute.mapping.username");

        //setup cache, use factory method to ensure singleton
        CacheManager.create();
        cache = CacheManager.getInstance().getCache(Constants.CACHE_NAME);

    }

    /**
     * Delegate to appropriate PortletMode.
     */
    protected void doDispatch(RenderRequest request, RenderResponse response) throws PortletException, IOException {
        log.debug("doDispatch()");

        //set global config
        getGlobalConfiguration(request);

        if (StringUtils.equalsIgnoreCase(request.getPortletMode().toString(), "CONFIG")) {
            doConfig(request, response);
        } else {
            super.doDispatch(request, response);
        }
    }

    /**
     * Process any portlet actions. 
     */

    public void processAction(ActionRequest request, ActionResponse response) {
        log.debug("processAction()");

        //check mode and delegate
        if (StringUtils.equalsIgnoreCase(request.getPortletMode().toString(), "CONFIG")) {
            processConfigAction(request, response);
        } else if (StringUtils.equalsIgnoreCase(request.getPortletMode().toString(), "EDIT")) {
            processEditAction(request, response);
        } else {
            log.error("No handler for PortletMode: " + request.getPortletMode().toString());
        }
    }

    /**
     * Helper to process CONFIG mode actions
     * @param request
     * @param response
     */
    private void processConfigAction(ActionRequest request, ActionResponse response) {
        log.debug("processConfigAction()");

        boolean success = true;
        PortletPreferences prefs = request.getPreferences();

        //get params and validate 
        try {
            prefs.setValue("key", request.getParameter("key"));
            prefs.setValue("secret", request.getParameter("secret"));
            prefs.setValue("endpoint", request.getParameter("endpoint"));
            prefs.setValue("adminUsername", request.getParameter("adminUsername"));
            prefs.setValue("adminPassword", request.getParameter("adminPassword"));
            prefs.setValue("loginUrl", request.getParameter("loginUrl"));
            prefs.setValue("scriptUrl", request.getParameter("scriptUrl"));
            prefs.setValue("allowedTools", request.getParameter("allowedTools"));
            prefs.setValue("portletTitle", request.getParameter("portletTitle"));

        } catch (ReadOnlyException e) {
            success = false;
            response.setRenderParameter("errorMessage", Messages.getString("error.form.readonly.error"));
            log.error(e);
        }

        //save them
        if (success) {
            try {
                prefs.store();
                response.setPortletMode(PortletMode.VIEW);
            } catch (ValidatorException e) {
                response.setRenderParameter("errorMessage", e.getMessage());
                log.error(e);
            } catch (IOException e) {
                response.setRenderParameter("errorMessage", Messages.getString("error.form.save.error"));
                log.error(e);
            } catch (PortletModeException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Helper to process EDIT mode actions
     * @param request
     * @param response
     */
    private void processEditAction(ActionRequest request, ActionResponse response) {
        log.debug("processEditAction()");

        replayForm = false;
        isValid = false;

        //get prefs and submitted values
        PortletPreferences prefs = request.getPreferences();
        String portletHeight = request.getParameter("portletHeight");
        String portletTitle = StringEscapeUtils.escapeHtml(StringUtils.trim(request.getParameter("portletTitle")));
        String remoteSiteId = request.getParameter("remoteSiteId");
        String remoteToolId = request.getParameter("remoteToolId");

        //catch a blank remoteSiteId and replay form
        if (StringUtils.isBlank(remoteSiteId)) {
            replayForm = true;
            response.setRenderParameter("portletTitle", portletTitle);
            response.setRenderParameter("portletHeight", portletHeight);
            return;
        }

        //catch a blank remoteToolId and replay form
        if (StringUtils.isBlank(remoteToolId)) {
            replayForm = true;
            response.setRenderParameter("portletTitle", portletTitle);
            response.setRenderParameter("portletHeight", portletHeight);
            response.setRenderParameter("remoteSiteId", remoteSiteId);
            return;
        }

        //portlet title could be blank, set to default
        //if(StringUtils.isBlank(portletTitle)){
        //   portletTitle=Constants.PORTLET_TITLE_DEFAULT;
        //}

        //form ok so validate
        try {
            prefs.setValue("portletHeight", portletHeight);

            //only set title if it is not blank
            if (StringUtils.isNotBlank(portletTitle)) {
                prefs.setValue("portletTitle", portletTitle);
            }

            prefs.setValue("remoteSiteId", remoteSiteId);
            prefs.setValue("remoteToolId", remoteToolId);
        } catch (ReadOnlyException e) {
            replayForm = true;
            response.setRenderParameter("errorMessage", Messages.getString("error.form.readonly.error"));
            log.error(e);
            return;
        }

        //save them
        try {
            prefs.store();
            isValid = true;
        } catch (ValidatorException e) {
            replayForm = true;
            response.setRenderParameter("errorMessage", e.getMessage());
            log.error(e);
            return;
        } catch (IOException e) {
            replayForm = true;
            response.setRenderParameter("errorMessage", Messages.getString("error.form.save.error"));
            log.error(e);
            return;
        }

        //if ok, invalidate cache and return to view
        if (isValid) {

            try {
                response.setPortletMode(PortletMode.VIEW);
            } catch (PortletModeException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Custom mode handler for CONFIG view
     */
    protected void doConfig(RenderRequest request, RenderResponse response) throws PortletException, IOException {
        log.debug("doConfig()");

        //set global settings into scope and dispatch
        request.setAttribute("key", key);
        request.setAttribute("secret", secret);
        request.setAttribute("endpoint", endpoint);
        request.setAttribute("adminUsername", adminUsername);
        request.setAttribute("adminPassword", adminPassword);
        request.setAttribute("loginUrl", loginUrl);
        request.setAttribute("scriptUrl", scriptUrl);
        request.setAttribute("allowedTools", allowedTools);
        request.setAttribute("portletTitle", getTitle(request));

        dispatch(request, response, configUrl);
    }

    /**
     * Render the main view
     */
    protected void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException {
        log.debug("doView()");

        //get data
        Map<String, String> launchData = getLaunchData(request, response);

        //catch - errors already handled
        if (launchData == null) {
            return;
        }

        //setup the params, serialise to a URL
        StringBuilder proxy = new StringBuilder();
        proxy.append(request.getContextPath());
        proxy.append(proxyUrl);
        proxy.append("?");
        proxy.append(HttpSupport.serialiseMapToQueryString(launchData));

        request.setAttribute("proxyContextUrl", proxy.toString());
        request.setAttribute("preferredHeight", getPreferredPortletHeight(request));

        dispatch(request, response, viewUrl);
    }

    /**
     * Render the edit page, invalidates any cached data
     */
    protected void doEdit(RenderRequest request, RenderResponse response) throws PortletException, IOException {
        log.debug("doEdit()");

        //check the global Sakai configuration is set
        if (StringUtils.isBlank(adminUsername) || StringUtils.isBlank(adminPassword)
                || StringUtils.isBlank(loginUrl) || StringUtils.isBlank(scriptUrl)
                || StringUtils.isBlank(allowedTools)) {
            log.error("Sakai configuration incomplete or missing. Please configure this portlet.");
            doError("error.no.sakai.config", "error.heading.general", request, response);
            return;
        }

        //setup the web service bean
        SakaiWebServiceLogic logic = new SakaiWebServiceLogic();
        logic.setAdminUsername(adminUsername);
        logic.setAdminPassword(adminPassword);
        logic.setLoginUrl(loginUrl);
        logic.setScriptUrl(scriptUrl);
        request.setAttribute("logic", logic);

        //setup remote userId
        String remoteUserId = getRemoteUserId(request, logic);
        if (StringUtils.isBlank(remoteUserId)) {
            log.error("No user info was returned from remote server.");
            doError("error.no.remote.data", "error.heading.general", request, response);
            return;
        }

        request.setAttribute("eid", getAuthenticatedUsername(request));
        request.setAttribute("remoteUserId", remoteUserId);

        // get list of sites
        List<Site> sites = getRemoteSitesForUser(request, logic);
        if (sites.isEmpty()) {
            log.error("No sites were returned from remote server.");
            doError("error.no.remote.data", "error.heading.general", request, response);
            return;
        }
        request.setAttribute("remoteSites", sites);

        //set list of allowed tool registrations
        request.setAttribute("allowedToolIds", Arrays.asList(StringUtils.split(allowedTools, ':')));

        //do we need to replay the form? This could be due to an error, or we need to show the lists again.
        //if so, use the original request params
        //otherwise, use the preferences
        if (replayForm) {

            String portletTitle = request.getParameter("portletTitle");
            String portletHeight = request.getParameter("portletHeight");
            String remoteSiteId = request.getParameter("remoteSiteId");
            String remoteToolId = request.getParameter("remoteToolId");

            if (StringUtils.isBlank(portletTitle)) {
                portletTitle = getPreferredPortletTitle(request);
            }

            if (StringUtils.isBlank(portletHeight)) {
                portletHeight = String.valueOf(getPreferredPortletHeight(request));
            }

            if (StringUtils.isBlank(remoteSiteId)) {
                remoteSiteId = getPreferredRemoteSiteId(request);
            }

            if (StringUtils.isBlank(remoteToolId)) {

                //if the request site and preference site are equal, use the preference
                //do not do this in any other case though since we are changing the site value and the tool list needs to be reset.
                String preferredRemoteSiteId = getPreferredRemoteSiteId(request);
                if (StringUtils.equals(remoteSiteId, preferredRemoteSiteId)) {
                    remoteToolId = getPreferredRemoteToolId(request);
                }
            }

            request.setAttribute("preferredPortletHeight", portletHeight);
            request.setAttribute("preferredPortletTitle", portletTitle);
            request.setAttribute("preferredRemoteSiteId", remoteSiteId);
            request.setAttribute("preferredRemoteToolId", remoteToolId);
            request.setAttribute("errorMessage", request.getParameter("errorMessage"));
        } else {
            request.setAttribute("preferredPortletHeight", getPreferredPortletHeight(request));
            request.setAttribute("preferredPortletTitle", getPreferredPortletTitle(request));
            request.setAttribute("preferredRemoteSiteId", getPreferredRemoteSiteId(request));
            request.setAttribute("preferredRemoteToolId", getPreferredRemoteToolId(request));

            //invalidate the cache for this item as it may change (only need to do this once per edit page view)
            evictFromCache(getPortletNamespace(response));
        }

        //cancel url
        request.setAttribute("cancelUrl", getPortletModeUrl(response, PortletMode.VIEW));

        dispatch(request, response, editUrl);
    }

    /**
     * Get the current user's details, exposed via portlet.xml
     * @param request
     * @return Map<String,String> of info
     */
    @SuppressWarnings("unchecked")
    private Map<String, String> getUserInfo(RenderRequest request) {
        return (Map<String, String>) request.getAttribute(PortletRequest.USER_INFO);
    }

    /**
     * Gets the unique namespace for this portlet
     * @param response
     * @return
     */
    private String getPortletNamespace(RenderResponse response) {
        return response.getNamespace();
    }

    /**
     * Setup the Map of params for the request
     * @param request
     * @param response
     * @return Map of params or null if any required data is missing
     */
    private Map<String, String> getLaunchData(RenderRequest request, RenderResponse response) {

        //check the global Basic LTI configuration is set
        if (StringUtils.isBlank(key) || StringUtils.isBlank(secret) || StringUtils.isBlank(endpoint)
                || StringUtils.isBlank(scriptUrl)) {
            log.error("Basic LTI configuration incomplete or missing. Please configure this portlet.");
            doError("error.no.basiclti.config", "error.heading.general", request, response);
            return null;
        }

        //check the global Sakai configuration is set
        if (StringUtils.isBlank(adminUsername) || StringUtils.isBlank(adminPassword)
                || StringUtils.isBlank(loginUrl) || StringUtils.isBlank(scriptUrl)) {
            log.error("Sakai configuration incomplete or missing. Please configure this portlet.");
            doError("error.no.sakai.config", "error.heading.general", request, response);
            return null;
        }

        //launch map
        Map<String, String> params;

        //check cache, otherwise form up all of the data
        String cacheKey = getPortletNamespace(response);
        params = retrieveFromCache(cacheKey);
        if (params == null) {

            //init for new data
            params = new HashMap<String, String>();

            //get site prefs
            String preferredRemoteSiteId = getPreferredRemoteSiteId(request);
            if (StringUtils.isBlank(preferredRemoteSiteId)) {
                doError("error.no.config", "error.heading.config", getPortletModeUrl(response, PortletMode.EDIT),
                        request, response);
                return null;
            }

            //get tool prefs
            String preferredRemoteToolId = getPreferredRemoteToolId(request);
            if (StringUtils.isBlank(preferredRemoteToolId)) {
                doError("error.no.config", "error.heading.config", getPortletModeUrl(response, PortletMode.EDIT),
                        request, response);
                return null;
            }

            //setup the web service bean
            SakaiWebServiceLogic logic = new SakaiWebServiceLogic();
            logic.setAdminUsername(adminUsername);
            logic.setAdminPassword(adminPassword);
            logic.setLoginUrl(loginUrl);
            logic.setScriptUrl(scriptUrl);

            //get remote userId
            String remoteUserId = getRemoteUserId(request, logic);
            if (StringUtils.isBlank(remoteUserId)) {
                doError("error.no.remote.data", "error.heading.general", request, response);
                return null;
            }

            //setup full endpoint
            params.put("endpoint_url", endpoint + preferredRemoteToolId);

            //required fields
            params.put("user_id", getAuthenticatedUsername(request));
            params.put("lis_person_name_given", null);
            params.put("lis_person_name_family", null);
            params.put("lis_person_name_full", null);
            params.put("lis_person_contact_email_primary", null);
            params.put("resource_link_id", getPortletNamespace(response));
            params.put("context_id", preferredRemoteSiteId);
            params.put("tool_consumer_instance_guid", key);
            params.put("lti_version", "LTI-1p0");
            params.put("lti_message_type", "basic-lti-launch-request");
            params.put("oauth_callback", "about:blank");
            params.put("basiclti_submit", "Launch Endpoint with BasicLTI Data");
            params.put("user_id", remoteUserId);

            //additional fields
            params.put("remote_tool_id", preferredRemoteToolId);

            //cache the data, must be done before signing
            updateCache(cacheKey, params);
        }

        if (log.isDebugEnabled()) {
            log.debug("Parameter map before OAuth signing");
            CollectionsSupport.printMap(params);
        }

        //sign the properties map
        params = OAuthSupport.signProperties(params.get("endpoint_url"), params, "POST", key, secret);

        if (log.isDebugEnabled()) {
            log.warn("Parameter map after OAuth signing");
            CollectionsSupport.printMap(params);
        }

        return params;
    }

    /**
     * Get the preferred portlet height if set, or default from Constants
     * @param request
     * @return
     */
    private int getPreferredPortletHeight(RenderRequest request) {
        PortletPreferences pref = request.getPreferences();
        return Integer.parseInt(pref.getValue("portletHeight", String.valueOf(Constants.PORTLET_HEIGHT_DEFAULT)));
    }

    /**
     * Get the preferred portlet title if set, or default from Constants
     * @param request
     * @return
     */
    private String getPreferredPortletTitle(RenderRequest request) {
        PortletPreferences pref = request.getPreferences();
        return pref.getValue("portletTitle", Constants.PORTLET_TITLE_DEFAULT);
    }

    /**
     * Get the preferred remote site id, or null if they have not made a choice yet
     * @param request
     * @return
     */
    private String getPreferredRemoteSiteId(RenderRequest request) {
        PortletPreferences pref = request.getPreferences();
        return pref.getValue("remoteSiteId", null);
    }

    /**
     * Get the preferred remote tool id, or null if they have not made a choice yet
     * @param request
     * @return
     */
    private String getPreferredRemoteToolId(RenderRequest request) {
        PortletPreferences pref = request.getPreferences();
        return pref.getValue("remoteToolId", null);
    }

    /**
     * Get the current username
     * @param request
     * @return
     */
    private String getAuthenticatedUsername(RenderRequest request) {
        Map<String, String> userInfo = getUserInfo(request);
        //return userInfo.get("username");
        return userInfo.get(attributeMappingForUsername);
    }

    /**
     * Get the remote userId for this user, either from session or from remote service
     * @param request
     * @param logic
     * @return
     */
    private String getRemoteUserId(RenderRequest request, SakaiWebServiceLogic logic) {

        String remoteUserId = (String) request.getPortletSession().getAttribute("remoteUserId");
        if (StringUtils.isBlank(remoteUserId)) {
            remoteUserId = SakaiWebServiceHelper.getRemoteUserIdForUser(logic, getAuthenticatedUsername(request));
            request.getPortletSession().setAttribute("remoteUserId", remoteUserId);
        }

        return remoteUserId;
    }

    /**
     * Get the list of remote sites for this user
     * @param logic
     * @return
     */
    private List<Site> getRemoteSitesForUser(RenderRequest request, SakaiWebServiceLogic logic) {
        return SakaiWebServiceHelper.getAllSitesForUser(logic, getAuthenticatedUsername(request));
    }

    /**
     * Override GenericPortlet.getTitle() to use the preferred title for the portlet instead
     */
    @Override
    protected String getTitle(RenderRequest request) {
        return getPreferredPortletTitle(request);
    }

    /**
     * Get the global config that is setup in the config mode
     * @param request
     */
    private void getGlobalConfiguration(RenderRequest request) {

        PortletPreferences pref = request.getPreferences();

        key = pref.getValue("key", null);
        secret = pref.getValue("secret", null);
        endpoint = pref.getValue("endpoint", null);
        adminUsername = pref.getValue("adminUsername", null);
        adminPassword = pref.getValue("adminPassword", null);
        loginUrl = pref.getValue("loginUrl", null);
        scriptUrl = pref.getValue("scriptUrl", null);
        allowedTools = pref.getValue("allowedTools", null);

    }

    /**
     * Helper to handle error messages
     * @param messageKey   Message bundle key
     * @param headingKey   optional error heading message bundle key, if not specified, the general one is used
     * @param request
     * @param response
     */
    private void doError(String messageKey, String headingKey, RenderRequest request, RenderResponse response) {
        doError(messageKey, headingKey, null, request, response);
    }

    /**
     * Helper to handle error messages
     * @param messageKey   Message bundle key
     * @param headingKey   optional error heading message bundle key, if not specified, the general one is used
     * @param link         if the message text is to be linked, what is the href?
     * @param request
     * @param response
     */
    private void doError(String messageKey, String headingKey, String link, RenderRequest request,
            RenderResponse response) {

        //message
        request.setAttribute("errorMessage", Messages.getString(messageKey));

        //optional heading
        if (StringUtils.isNotBlank(headingKey)) {
            request.setAttribute("errorHeading", Messages.getString(headingKey));
        } else {
            request.setAttribute("errorHeading", Messages.getString("error.heading.general"));
        }

        if (StringUtils.isNotBlank(link)) {
            request.setAttribute("errorLink", link);
        }

        //dispatch
        try {
            dispatch(request, response, errorUrl);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Dispatch to a JSP or servlet
     * @param request
     * @param response
     * @param path
     * @throws PortletException
     * @throws IOException
     */
    protected void dispatch(RenderRequest request, RenderResponse response, String path)
            throws PortletException, IOException {
        response.setContentType("text/html");
        PortletRequestDispatcher dispatcher = getPortletContext().getRequestDispatcher(path);
        dispatcher.include(request, response);
    }

    /**
     * Helper to evict an item from a cache. If we visit the edit mode, we must evict the current data. It will be re-cached later.
     * @param cacheKey   the id for the data in the cache
     */
    private void evictFromCache(String cacheKey) {
        cache.remove(cacheKey);
        log.debug("Evicted data in cache for key: " + cacheKey);
    }

    /**
     * Helper to retrieve data from the cache
     * @param key
     * @return
     */
    private Map<String, String> retrieveFromCache(String key) {
        Element element = cache.get(key);
        if (element != null) {
            Map<String, String> data = (Map<String, String>) element.getObjectValue();
            log.debug("Fetching data from cache for key: " + key);
            return data;
        }
        return null;
    }

    /**
     * Helper to update the cache
     * @param cacheKey   the id for the data in the cache   
     * @param data      the data to be assocaited with that key in the cache
     */
    private void updateCache(String cacheKey, Map<String, String> data) {
        cache.put(new Element(cacheKey, data));
        log.debug("Added data to cache for key: " + cacheKey);
    }

    /**
     * Helper to get the URL to take us to a portlet mode.
     * This will end up in doDispatch.
     * 
     * @param response
     * @return
     */
    private String getPortletModeUrl(RenderResponse response, PortletMode mode) {

        PortletURL url = response.createRenderURL();
        try {
            url.setPortletMode(mode);
        } catch (PortletModeException e) {
            log.error("Invalid portlet mode: " + mode);
            return null;
        }

        return url.toString();
    }

    public void destroy() {
        log.info("destroy()");
        CacheManager.getInstance().shutdown();
    }

}