/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/tool/impl/ActiveToolComponent.java $ * $Id: ActiveToolComponent.java 120927 2013-03-08 13:06:14Z azeckoski@unicon.net $ *********************************************************************************** * * Copyright (c) 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.tool.impl; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.authz.api.FunctionManager; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.tool.api.ActiveTool; import org.sakaiproject.tool.api.ActiveToolManager; import org.sakaiproject.tool.api.Placement; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.tool.api.Tool; import org.sakaiproject.tool.api.ToolException; import org.sakaiproject.tool.api.ToolSession; import org.sakaiproject.util.Xml; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * <p> * ActiveToolComponent is the standard implementation of the Sakai ActiveTool API. * </p> */ public abstract class ActiveToolComponent extends ToolComponent implements ActiveToolManager { /** Our log (commons). */ private static Log M_log = LogFactory.getLog(ActiveToolComponent.class); public static final String TOOL_PORTLET_CONTEXT_PATH = "portlet-context"; static final String TOOL_CATEGORIES_PREFIX = "tool.categories."; static final String TOOL_CATEGORIES_APPEND_PREFIX = TOOL_CATEGORIES_PREFIX+"append."; // private ResourceLoader toolProps = null; /********************************************************************************************************************************************************************************************************************************************************** * Dependencies *********************************************************************************************************************************************************************************************************************************************************/ /** * @return the SessionManager collaborator. */ protected abstract SessionManager sessionManager(); /** * @return the FunctionManager collaborator. */ protected abstract FunctionManager functionManager(); /** * @ the serverConfigurationService() collaborator. */ protected abstract ServerConfigurationService serverConfigurationService(); /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ /********************************************************************************************************************************************************************************************************************************************************** * Work interface methods: ActiveToolManager - ToolManager is covered by our base class ToolComponent *********************************************************************************************************************************************************************************************************************************************************/ /** * @inheritDoc */ public void register(Tool tool, ServletContext context) { ActiveTool at = null; // make it an active tool if (tool instanceof MyActiveTool) { at = (MyActiveTool) tool; } else if (tool instanceof ActiveTool) { at = (ActiveTool) tool; } else { at = new MyActiveTool(tool); } // TODO: elevate setServletContext to ActiveTool interface to avoid instance testing if (at instanceof MyActiveTool) { ((MyActiveTool) at).setServletContext(context); } // KNL-409 - JSR-168 Portlets do not dispatch the same as normal // Sakai tools - so the warning below is not necessary for JSR-168 // tools String portletContext = null; Properties toolProps = at.getFinalConfig(); if (toolProps != null) { portletContext = toolProps .getProperty(TOOL_PORTLET_CONTEXT_PATH); } // KNL-352 - in Websphere ServletContext.getNamedDispatcher(...) will initialize the given Servlet. // However Websphere's normal Servlet initialization happens later at com.ibm.ws.wswebcontainer.webapp.WebApp.initialize(WebApp.java:293). // As a result, Websphere ends up trying to initialize the Servlet twice, causing the observed mapping clash exceptions. if (!"websphere".equals(serverConfigurationService().getString("servlet.container")) && portletContext == null ) { // try getting the RequestDispatcher, just to test - but DON'T SAVE IT! // Tomcat's RequestDispatcher is NOT thread safe and must be gotten from the context // every time its needed! RequestDispatcher dispatcher = context.getNamedDispatcher(at.getId()); if (dispatcher == null) { M_log.warn("missing dispatcher for tool: " + at.getId()); } } m_tools.put(at.getId(), at); } /** * @inheritDoc */ public void register(Document toolXml, ServletContext context) { if (toolXml == null) { M_log.info("register: invalid or empty tool registration document"); return; } Element root = toolXml.getDocumentElement(); if (!root.getTagName().equals("registration")) { M_log.info("register: invalid root element (expecting \"registration\"): " + root.getTagName()); return; } // read the children nodes (tools) NodeList rootNodes = root.getChildNodes(); final int rootNodesLength = rootNodes.getLength(); for (int i = 0; i < rootNodesLength; i++) { Node rootNode = rootNodes.item(i); if (rootNode.getNodeType() != Node.ELEMENT_NODE) continue; Element rootElement = (Element) rootNode; // for tool if (rootElement.getTagName().equals("tool")) { org.sakaiproject.tool.impl.ToolImpl tool = parseToolRegistration(rootElement); register(tool, context); } // for function else if (rootElement.getTagName().equals("function")) { String function = rootElement.getAttribute("name").trim(); functionManager().registerFunction(function); } } } /** * @inheritDoc */ public List<Tool> parseTools(File toolXmlFile) { if ( ! toolXmlFile.canRead() ) return null; String path = toolXmlFile.getAbsolutePath(); if (!path.endsWith(".xml")) { M_log.info("register: skiping non .xml file: " + path); return null; } M_log.info("parse-file: " + path); Document doc = Xml.readDocument(path); if ( doc == null ) return null; return parseTools(doc); } /** * @inheritDoc */ public List<Tool> parseTools(InputStream toolXmlStream) { Document doc = Xml.readDocumentFromStream(toolXmlStream); try { toolXmlStream.close(); } catch (Exception e) { } if ( doc == null ) return null; return parseTools(doc); } /** * @inheritDoc */ public List<Tool> parseTools(Document toolXml) { List<Tool> retval = new ArrayList<Tool> (); Element root = toolXml.getDocumentElement(); if (!root.getTagName().equals("registration")) { M_log.info("register: invalid root element (expecting \"registration\"): " + root.getTagName()); return null; } // read the children nodes (tools) NodeList rootNodes = root.getChildNodes(); final int rootNodesLength = rootNodes.getLength(); for (int i = 0; i < rootNodesLength; i++) { Node rootNode = rootNodes.item(i); if (rootNode.getNodeType() != Node.ELEMENT_NODE) continue; Element rootElement = (Element) rootNode; // for tool if (rootElement.getTagName().equals("tool")) { org.sakaiproject.tool.impl.ToolImpl tool = parseToolRegistration(rootElement); retval.add(tool); } } if ( retval.size() < 1 ) return null; return retval; } private org.sakaiproject.tool.impl.ToolImpl parseToolRegistration(Element rootElement) { org.sakaiproject.tool.impl.ToolImpl tool = new org.sakaiproject.tool.impl.ToolImpl(this); final String toolId = rootElement.getAttribute("id").trim(); tool.setId(toolId); final String toolTitle = getLocalizedToolProperty(toolId, "title"); final String toolDescription = getLocalizedToolProperty(toolId, "description"); // if the key is empty or absent, the length is 0. if so, use the string from the XML file if(toolTitle != null && toolTitle.length()>0) { tool.setTitle(toolTitle); } else { tool.setTitle(rootElement.getAttribute("title").trim()); } // if the key is empty or absent, the length is 0. if so, use the string from the XML file if(toolDescription != null && toolDescription.length()>0) { tool.setDescription(toolDescription); } else { tool.setDescription(rootElement.getAttribute("description").trim()); } tool.setHome(StringUtils.trimToNull(rootElement.getAttribute("home"))); if ("tool".equals(rootElement.getAttribute("accessSecurity"))) { tool.setAccessSecurity(Tool.AccessSecurity.TOOL); } else { tool.setAccessSecurity(Tool.AccessSecurity.PORTAL); } // collect values for these collections Properties finalConfig = new Properties(); Properties mutableConfig = new Properties(); Set categories = new HashSet(); Set keywords = new HashSet(); NodeList kids = rootElement.getChildNodes(); final int kidsLength = kids.getLength(); for (int k = 0; k < kidsLength; k++) { Node kidNode = kids.item(k); if (kidNode.getNodeType() != Node.ELEMENT_NODE) continue; Element kidElement = (Element) kidNode; // for configuration if (kidElement.getTagName().equals("configuration")) { String name = kidElement.getAttribute("name").trim(); String value = kidElement.getAttribute("value").trim(); String type = kidElement.getAttribute("type").trim(); if (name.length() > 0) { if ("final".equals(type)) { finalConfig.put(name, value); } else { mutableConfig.put(name, value); } } } // for category if (kidElement.getTagName().equals("category")) { String name = kidElement.getAttribute("name").trim(); if (name.length() > 0) { categories.add(name); } } // for keyword if (kidElement.getTagName().equals("keyword")) { String name = kidElement.getAttribute("name").trim(); if (name.length() > 0) { keywords.add(name); } } } // KNL-1031 - Override OR Add additional categories from sakai.properties String[] categoriesArray = serverConfigurationService().getStrings(TOOL_CATEGORIES_PREFIX+tool.getId()); if (categoriesArray != null) { // override the set of categories categories.clear(); for (String category: categoriesArray) { if (StringUtils.isNotBlank(category)) { categories.add(category); } } } else { categoriesArray = serverConfigurationService().getStrings(TOOL_CATEGORIES_APPEND_PREFIX+tool.getId()); if (categoriesArray != null) { // add categories instead of overriding for (String category: categoriesArray) { if (StringUtils.isNotBlank(category)) { categories.add(category); } } } } // set the tool's collected values tool.setRegisteredConfig(finalConfig, mutableConfig); tool.setCategories(categories); tool.setKeywords(keywords); return tool; } /** * @inheritDoc */ public void register(File toolXmlFile, ServletContext context) { String path = toolXmlFile.getAbsolutePath(); if (!path.endsWith(".xml")) { M_log.info("register: skiping non .xml file: " + path); return; } M_log.info("register: file: " + path); Document doc = Xml.readDocument(path); register(doc, context); } /** * @inheritDoc */ public void register(InputStream toolXmlStream, ServletContext context) { Document doc = Xml.readDocumentFromStream(toolXmlStream); try { toolXmlStream.close(); } catch (Exception e) { } register(doc, context); } /** * @inheritDoc */ public ActiveTool getActiveTool(String id) { if (id == null) return null; return (ActiveTool) m_tools.get(id); } /********************************************************************************************************************************************************************************************************************************************************** * Entity: ActiveTool *********************************************************************************************************************************************************************************************************************************************************/ public class MyActiveTool extends org.sakaiproject.tool.impl.ToolImpl implements ActiveTool { protected ServletContext m_servletContext = null; /** * Construct */ public MyActiveTool() { super(ActiveToolComponent.this); } public void setServletContext(ServletContext context) { m_servletContext = context; } /** * Construct from a Tool */ public MyActiveTool(org.sakaiproject.tool.api.Tool t) { super(ActiveToolComponent.this); this.m_categories.addAll(t.getCategories()); this.m_mutableConfig.putAll(t.getMutableConfig()); this.m_finalConfig.putAll(t.getFinalConfig()); this.m_keywords.addAll(t.getKeywords()); this.m_id = t.getId(); this.m_title = t.getTitle(); this.m_description = t.getDescription(); this.m_accessSecurity = t.getAccessSecurity(); this.m_home = t.getHome(); } /** * Return a RequestDispatcher that can be used to dispatch to this tool - RequestDispatcher is NOT THREAD-SAFE FOR REUSE, so this method must be called each and every time a request is forwarded. */ protected RequestDispatcher getDispatcher() { return m_servletContext.getNamedDispatcher(getId()); } /** * @inheritDoc */ public void forward(HttpServletRequest req, HttpServletResponse res, Placement placement, String toolContext, String toolPath) throws ToolException { WrappedRequest wreq = null; try { wreq = new WrappedRequest(req, toolContext, toolPath, placement, false, this); WrappedResponse wres = new WrappedResponse(wreq, res); RequestDispatcher dispatcher = getDispatcher(); if (dispatcher == null) { throw new IllegalArgumentException("Unable to find registered context for tool with ID " + getId()); } dispatcher.forward(wreq, wres); } catch (IOException e) { throw new ToolException(e); } catch (ToolException e) { throw e; } catch (ServletException e) { throw new ToolException(e); } finally { if (wreq != null) wreq.cleanup(); } } /** * @inheritDoc */ public void include(HttpServletRequest req, HttpServletResponse res, Placement placement, String toolContext, String toolPath) throws ToolException { WrappedRequest wreq = null; try { wreq = new WrappedRequest(req, toolContext, toolPath, placement, true, this); getDispatcher().include(wreq, res); } catch (IOException e) { throw new ToolException(e); } catch (ToolException e) { throw e; } catch (ServletException e) { throw new ToolException(e); } finally { if (wreq != null) wreq.cleanup(); } } /** * @inheritDoc */ public void help(HttpServletRequest req, HttpServletResponse res, String toolContext, String toolPath) throws ToolException { // fragment? boolean fragment = Boolean.TRUE.toString().equals(req.getAttribute(FRAGMENT)); WrappedRequest wreq = null; try { wreq = new WrappedRequest(req, toolContext, toolPath, null, fragment, this); if (fragment) { getDispatcher().include(wreq, res); } else { getDispatcher().forward(wreq, res); } } catch (IOException e) { throw new ToolException(e); } catch (ToolException e) { throw e; } catch (ServletException e) { throw new ToolException(e); } finally { if (wreq != null) wreq.cleanup(); } } /** * Wraps a request object so we can override some standard behavior. */ public class WrappedRequest extends HttpServletRequestWrapper { /** The context to report. */ protected String m_context = null; /** The pathInfo to report. */ protected String m_path = null; /** attributes to override the wrapped req's attributes. */ protected Map m_attributes = new HashMap(); /** The prior current tool session, to restore after we are done. */ protected ToolSession m_priorToolSession = null; /** The prior current tool, to restore after we are done. */ protected Tool m_priorTool = null; /** The prior current placementl, to restore after we are done. */ protected Placement m_priorPlacement = null; /** The request object we wrap. */ protected HttpServletRequest m_wrappedReq = null; public WrappedRequest(HttpServletRequest req, String context, String path, Placement placement, boolean fragment, Tool tool) { super(req); m_wrappedReq = req; m_context = context; // default if needed if (m_context == null) { m_context = req.getContextPath() + req.getServletPath() + req.getPathInfo(); } m_path = path; if (placement != null) { ToolSession ts = sessionManager().getCurrentSession().getToolSession(placement.getId()); // put the session in the request attribute setAttribute(TOOL_SESSION, ts); // set as the current tool session, and setup for undoing this later m_priorToolSession = sessionManager().getCurrentToolSession(); sessionManager().setCurrentToolSession(ts); // set this tool placement as current, in the request and for the service's "current" tool placement setAttribute(PLACEMENT, placement); setAttribute(PLACEMENT_ID, placement.getId()); m_priorPlacement = getCurrentPlacement(); setCurrentPlacement(placement); } setAttribute(FRAGMENT, Boolean.toString(fragment)); // set this tool as current, in the request and for the service's "current" tool setAttribute(TOOL, tool); m_priorTool = getCurrentTool(); setCurrentTool(tool); } private String requestURI; /** * Allows us to set the request URI to the correct value if desired * @param requestURI this should be the complete URL for this request (not counting the get params, everything after the ?) */ public void setRequestURI(String requestURI) { this.requestURI = requestURI; } @Override public StringBuffer getRequestURL() { StringBuffer sb = null; if (requestURI == null) { sb = super.getRequestURL(); } else { sb = new StringBuffer(requestURI); } // now attempt to autofix the URL sb.append( getURLSuffix(sb.toString()) ); return sb; }; @Override public String getRequestURI() { String uri = null; if (requestURI == null) { uri = super.getRequestURI(); } else { uri = requestURI; } // now attempt to autofix the URL uri += getURLSuffix(uri); return uri; } /** * This is here to fix up the directtool stuff, * http://jira.sakaiproject.org/jira/browse/SAK-8946, * it is mostly here to make the requestURL and pathinfo conform to the servlet spec<br/> * Used like so:<br/> * uri += getURLSuffix(uri); * * @param requestURL the current requestURL (probably invalid) * @return the suffix to append to the existing requestURL */ private String getURLSuffix(String requestURL) { // now attempt to autofix the URL StringBuilder sb = new StringBuilder(); if (requestURL != null) { String path = getPathInfo(); if (path != null) { if (! requestURL.contains(path)) { if (! path.startsWith("/")) { sb.append("/"); } sb.append(path); } } } return sb.toString(); } public String getPathInfo() { if (getAttribute(NATIVE_URL) != null) return super.getPathInfo(); return m_path; } public String getServletPath() { if (getAttribute(NATIVE_URL) != null) return super.getServletPath(); return ""; } public String getContextPath() { if (getAttribute(NATIVE_URL) != null) return super.getContextPath(); return m_context; } public Object getAttribute(String name) { if (m_attributes.containsKey(name)) { return m_attributes.get(name); } return super.getAttribute(name); } public Enumeration getAttributeNames() { Set s = new HashSet(); s.addAll(m_attributes.keySet()); for (Enumeration e = super.getAttributeNames(); e.hasMoreElements();) { String name = (String) e.nextElement(); s.add(name); } return new IteratorEnumeration(s.iterator()); } public void setAttribute(String name, Object value) { m_attributes.put(name, value); // if this is a special attribute, set it back through all wrapped requests of this class if (Tool.NATIVE_URL.equals(name)) { m_wrappedReq.setAttribute(name, value); } } public void removeAttribute(String name) { if (m_attributes.containsKey(name)) { m_attributes.remove(name); } else { super.removeAttribute(name); } // if this is a special attribute, set it back through all wrapped requests of this class if (Tool.NATIVE_URL.equals(name)) { m_wrappedReq.removeAttribute(name); } } public void cleanup() { // restore the tool, placement and tool session and tool placement config that was in effect before we changed it setCurrentTool(m_priorTool); sessionManager().setCurrentToolSession(m_priorToolSession); setCurrentPlacement(m_priorPlacement); } /************************************************************************************************************************************************************************************************************************************************** * Enumeration over an iterator *************************************************************************************************************************************************************************************************************************************************/ protected class IteratorEnumeration implements Enumeration { /** The iterator over which this enumerates. */ protected Iterator m_iterator = null; public IteratorEnumeration(Iterator i) { m_iterator = i; } public boolean hasMoreElements() { return m_iterator.hasNext(); } public Object nextElement() { return m_iterator.next(); } } } /** * Wraps a response object so we can override some standard behavior. */ public class WrappedResponse extends HttpServletResponseWrapper { /** The request. */ protected HttpServletRequest m_req = null; /** The wrapped response. */ protected HttpServletResponse m_res = null; public WrappedResponse(HttpServletRequest req, HttpServletResponse res) { super(res); m_req = req; m_res = res; } public String encodeRedirectUrl(String url) { return rewriteURL(url); } public String encodeRedirectURL(String url) { return rewriteURL(url); } public String encodeUrl(String url) { return rewriteURL(url); } public String encodeURL(String url) { return rewriteURL(url); } public void sendRedirect(String url) throws IOException { // SAK-13408 - Relative redirections are based on the request URI. This fix addresses the problem // of Websphere having a different request URI than Tomcat. Instead, the relative URL will be // converted to an absolute URL. if ("websphere".equals(serverConfigurationService().getString("servlet.container"))) { url = createAbsoluteURL(url); } super.sendRedirect(rewriteURL(url)); } /** * This method takes the given relative Sakai URL and uses the * context path and path info to create the corresponding * absolute URL. * * @param relativeUrl the relative URL to convert to an absolute URL * @return the absolute URL */ protected String createAbsoluteURL(String relativeUrl) { // ensure this is a relative URL if (!(relativeUrl.toLowerCase().startsWith("http")) && !(relativeUrl.startsWith("/"))) { ActiveToolComponent.MyActiveTool.WrappedRequest wr = (WrappedRequest) this.m_req; // need to obtain any extra path info from the path StringBuilder pathBuilder = new StringBuilder(""); if (wr.m_path != null) { StringTokenizer pathTokenizer = new StringTokenizer(wr.m_path, "/"); // if the path has more than one segment (eg. "/name1/name2") int numberOfPathElements = pathTokenizer.countTokens(); if (numberOfPathElements > 1) { // copy over everything but the last segment for (int i = 0; i < numberOfPathElements - 1; i++) { pathBuilder.append("/"); pathBuilder.append(pathTokenizer.nextToken()); } } } if (!pathBuilder.toString().endsWith("/")) { pathBuilder.append("/"); } relativeUrl = wr.m_context + pathBuilder.toString() + relativeUrl; } return relativeUrl; } /** * Rewrites the given URL to insert the current tool placement id, if any, as the start of the path * * @param url * The url to rewrite. */ protected String rewriteURL(String url) { // only if we are in native url mode - if in Sakai mode, don't include the placement id if (m_req.getAttribute(NATIVE_URL) != null) { // if we have a tool placement to add, add it Placement placement = (Placement) m_req.getAttribute(PLACEMENT); if (placement != null) { String placementId = placement.getId(); // compute the URL root "back" to this servlet // Note: this must not be pre-computed, as it can change as things are dispatched StringBuilder full = new StringBuilder(); full.append(m_req.getScheme()); full.append("://"); full.append(m_req.getServerName()); if (((m_req.getServerPort() != 80) && (!m_req.isSecure())) || ((m_req.getServerPort() != 443) && (m_req.isSecure()))) { full.append(":"); full.append(m_req.getServerPort()); } // include just the context path - anything to this context will get the encoding StringBuilder rel = new StringBuilder(); rel.append(m_req.getContextPath()); full.append(rel.toString()); // if we match the fullUrl, or the relUrl, assume that this is a URL back to this servlet context if ((url.startsWith(full.toString()) || url.startsWith(rel.toString()))) { // put the placementId in as a parameter StringBuilder newUrl = new StringBuilder(url); if (url.indexOf('?') != -1) { newUrl.append('&'); } else { newUrl.append('?'); } newUrl.append(Tool.PLACEMENT_ID); newUrl.append("="); newUrl.append(placementId); // TODO: (or not) let the wrapped resp. do the work, too return newUrl.toString(); } } } return url; } } } }