/* * #! * Ontopia Webed * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * 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 net.ontopia.topicmaps.webed.servlets; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.ServletException; import javax.servlet.UnavailableException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.core.TopicMapStoreIF; import net.ontopia.topicmaps.nav2.core.NavigatorApplicationIF; import net.ontopia.topicmaps.nav2.core.NavigatorRuntimeException; import net.ontopia.topicmaps.nav2.core.UserIF; import net.ontopia.topicmaps.nav2.utils.NavigatorUtils; import net.ontopia.topicmaps.schema.impl.osl.OSLSchema; import net.ontopia.topicmaps.webed.core.ActionIF; import net.ontopia.topicmaps.webed.core.ActionParametersIF; import net.ontopia.topicmaps.webed.core.ActionResponseIF; import net.ontopia.topicmaps.webed.core.ActionRuntimeException; import net.ontopia.topicmaps.webed.core.OSLSchemaAwareIF; import net.ontopia.topicmaps.webed.impl.actions.DefaultAction; import net.ontopia.topicmaps.webed.impl.actions.DummyAction; import net.ontopia.topicmaps.webed.impl.basic.ActionContext; import net.ontopia.topicmaps.webed.impl.basic.ActionContextIF; import net.ontopia.topicmaps.webed.impl.basic.ActionError; import net.ontopia.topicmaps.webed.impl.basic.ActionForwardPage; import net.ontopia.topicmaps.webed.impl.basic.ActionForwardPageIF; import net.ontopia.topicmaps.webed.impl.basic.ActionGroupIF; import net.ontopia.topicmaps.webed.impl.basic.ActionInGroup; import net.ontopia.topicmaps.webed.impl.basic.ActionParameters; import net.ontopia.topicmaps.webed.impl.basic.ActionRegistryIF; import net.ontopia.topicmaps.webed.impl.basic.ActionResponse; import net.ontopia.topicmaps.webed.impl.basic.ActionValidationException; import net.ontopia.topicmaps.webed.impl.basic.Constants; import net.ontopia.topicmaps.webed.impl.basic.NoActionDataFoundException; import net.ontopia.topicmaps.webed.impl.basic.ParamRuleIF; import net.ontopia.topicmaps.webed.impl.basic.UnavailableSessionException; import net.ontopia.topicmaps.webed.impl.basic.WebEdRequest; import net.ontopia.topicmaps.webed.impl.utils.ActionData; import net.ontopia.topicmaps.webed.impl.utils.NamedLockManager; import net.ontopia.topicmaps.webed.impl.utils.Parameters; import net.ontopia.topicmaps.webed.impl.utils.ReqParamUtils; import net.ontopia.topicmaps.webed.impl.utils.TagUtils; import net.ontopia.utils.DebugUtils; import net.ontopia.utils.OntopiaRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * INTERNAL: Central entry point to the web application for processing a HTTP * request and forwarding to the appropriate page depending on the executed * actions. */ public final class ProcessServlet extends HttpServlet { private static final long serialVersionUID = 1115457802266399626L; // --- initialize logging facility. static Logger logger = LoggerFactory.getLogger(ProcessServlet.class.getName()); /** * INTERNAL: Handles a HTTP GET request. */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } /** * INTERNAL: Handles a HTTP POST request. */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } /** * INTERNAL: Internal method which is for handling all of the incoming HTTP * requests. The following request parameters have to be available: * <ul> * <li>Constants.RP_TOPICMAP_ID: holds the ID of the topicmap working with</li> * <li>Constants.RP_ACTIONGROUP: containing the name of the current action * group</li> * <li>Other request parameters may be set and are specific to the current * action group.</li> * </ul> * </p> * <p> * Note: See the action configuration file * <code>WEB-INF/config/actions.xml</code> for more information. */ protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // get hold of navigator application NavigatorApplicationIF navApp = getNavigatorApplication(); // IMPORTANT: never call getParameter before doing the decoding // below (see bug #2096 for more information) // decode request parameters String charEnc = getCharacterEncoding(navApp); Parameters params = getParameters(request, navApp, charEnc); // unlock form String operation = request.getParameter(Constants.RP_OPERATION); if (operation != null && operation.equals(Constants.RPVAL_UNLOAD)) { unlock(request, false); return; } else if (operation != null && operation.equals(Constants.RPVAL_UNLOCK)) { unlock(request, true); return; } // initialization logrequest(params); if (params.get("ag") == null) { logger.error("Request parameter 'ag' is null, which should never happen." + "Therefore error-logging the following. (indented)"); String indentation = " "; errorLogrequest(params, indentation); logger.error(indentation + "request.getHeader(\"user-agent\")" + request.getHeader("user-agent")); logger.error(indentation + "request.getHeader(\"referer\")" + request.getHeader("referer")); logger.error(indentation + "getRemoteUser(): " + request.getRemoteUser()); logger.error(indentation + "getRemoteAddr(): " + request.getRemoteAddr()); } List nonCriticalErrors = new ArrayList(); UserIF user = getUser(request); // make sure that user still owns form lock NamedLockManager lockMan = null; String lock_varname = params.get(Constants.RP_LOCKVAR); if (lock_varname != null && !lock_varname.equals("") && !lock_varname.endsWith("-unlocked")) { lockMan = TagUtils.getNamedLockManager(getServletContext()); if (!lockMan.ownsLock(user, lock_varname)) { NoActionDataFoundException e = new NoActionDataFoundException("Lock on form expired (lock id=" + lock_varname + ")."); Map attrs = new HashMap(); attrs.put("referer", request.getHeader("referer")); attrs.put("linkforward", params.get("linkforward")); e.setUserObject(attrs); throw e; } } // request preparation ActionContextIF actionCtxt = new ActionContext(user, params); // first retrieve action registry (built from action configuration file) ActionRegistryIF registry; try { registry = TagUtils.getActionRegistry(request); } catch (javax.servlet.jsp.JspTagException e) { throw new OntopiaRuntimeException(e); } if (registry == null) throw new ServletException("The action registry is not available, " + "please verify your action configuration file."); String actionGroupName = params.get(Constants.RP_ACTIONGROUP); if (actionGroupName == null) throw new ServletException("The request parameter '" + Constants.RP_ACTIONGROUP + "' must " + "contain a declared action group name."); ActionGroupIF actionGroup = registry.getActionGroup(actionGroupName); if (actionGroup == null) logger.warn("No action group was found for '" + actionGroup + "'."); ActionForwardPageIF forwardPage = null; ActionResponseIF acresponse = new ActionResponse(request, response); // repeat required parameters (actions may override) for (int ix = 0; ix < Constants.OBJ_REQPARAMS.length; ix++) { String param = Constants.OBJ_REQPARAMS[ix]; String value = params.get(param); if (value != null && !value.equals("")) acresponse.addParameter(param, value); } // retrieve rw topicmap from navigator application String topicmapId = params.get(Constants.RP_TOPICMAP_ID); TopicMapIF topicmap = null; TopicMapStoreIF store = null; try { // txn block try { topicmap = (topicmapId == null ? null : navApp.getTopicMapById(topicmapId, false)); } catch (NavigatorRuntimeException e) { throw new UnavailableException(e.getMessage()); } if (topicmap == null) logger.error("Topic map (" + topicmapId + ") is NOT available."); else logger.info("Topic map (" + topicmapId + ") is available."); // get topic map store and synchronize on it store = (topicmap == null ? null : topicmap.getStore()); Object lock = (store == null ? new Object() : store); synchronized (lock) { // and set it as a request attribute (can be retrieved via actionCtxt) request.setAttribute(Constants.RA_TOPICMAP, topicmap); // setup schema information Map schemaRegistry = TagUtils.getSchemaRegistry(getServletContext()); OSLSchema schema = (OSLSchema) schemaRegistry.get(topicmapId); // --- (A): find all actions to be executed // Create WebEdRequest object Map actionmap = new HashMap(); WebEdRequest werequest = new WebEdRequest(user, actionmap, getServletContext(), request); // collect all action information ActionData exclusive = null; List actions = new ArrayList(); List conditionalActions = new ArrayList(); // will only run if others run Iterator it; try { it = actionCtxt.getAllActions().iterator(); while (it.hasNext()) { ActionData data = (ActionData) it.next(); ActionInGroup action = data.getAction(); String param = data.getFieldName(); String[] values = actionCtxt.getParameterValues(param); // add action parameters to webed request's action map ActionParametersIF aparams = new ActionParameters(param, values, params.getFile(param), TagUtils.deserializeParameters(data.getParameters(), topicmap), topicmap, werequest); actionmap.put(data.getAction().getName(), aparams); // check to see if action is to be executed if (!isValueEqual(param, values, data.getValue())) { if (data.getRunIfNoChanges()) actions.add(data); // this should run even if there are no // changes else conditionalActions.add(data); // only run this if another action // runs if (action.isExclusive()) // continue so we complete actionmap, then act on this later exclusive = data; } } } catch (NoActionDataFoundException e) { Map attrs = new HashMap(); attrs.put("referer", request.getHeader("referer")); attrs.put("linkforward", params.get("linkforward")); e.setUserObject(attrs); throw e; } if (exclusive != null) { // this action is exclusive, so only execute that, and skip all others logger.debug("Action " + exclusive + " was exclusive; not running any others"); actions = Collections.singletonList(exclusive); conditionalActions = Collections.EMPTY_LIST; } // put actions in right order (and filter them, apparently) actions = sortActions(actions, actionGroup); // add conditional actions, if any if (changesMade(actions) && !conditionalActions.isEmpty()) { logger.debug("Other actions are run, so conditional actions are run, too"); actions.addAll(conditionalActions); // adding conditional actions to list // resort, to get new actions into right order actions = sortActions(actions, actionGroup); } // --- (B): execute actions // trigger all actions to be triggered it = actions.iterator(); while (it.hasNext()) { ActionData data = (ActionData) it.next(); boolean actionHadErrors = false; String param = data.getFieldName(); String[] values = actionCtxt.getParameterValues(param); ActionInGroup action = data.getAction(); if (action.getAction() instanceof OSLSchemaAwareIF) ((OSLSchemaAwareIF) action.getAction()).setSchema(schema); ActionParametersIF aparams = new ActionParameters(param, values, params.getFile(param), TagUtils.deserializeParameters(data.getParameters(), topicmap), topicmap, werequest); try { logger.debug("Invoking action " + aparams + ", " + action); action.getAction().perform(aparams, acresponse); } catch (ActionRuntimeException e) { logger.error("Action " + aparams + " raised error", e); if (e.getCritical()) throw e; else { actionHadErrors = true; nonCriticalErrors.add(new ActionError(e, data, values)); } } // record that we've executed an action (doing it here so // run-if-changes=false actions won't trigger themselves) werequest.setActionsExecuted(true); // executing sub-actions Iterator it2 = data.getSubActions().iterator(); while (it2.hasNext()) { ActionData subdata = (ActionData) it2.next(); ActionInGroup subaction = subdata.getAction(); if (subaction instanceof OSLSchemaAwareIF) ((OSLSchemaAwareIF) subaction).setSchema(schema); ActionParametersIF subparams = new ActionParameters(null, null, null, TagUtils.deserializeParameters(subdata.getParameters(), topicmap), topicmap, werequest); try { logger.debug("Invoking sub-action of " + param + ": " + subaction); subaction.getAction().perform(subparams, acresponse); } catch (ActionRuntimeException e) { logger.error("Sub-action of " + params + " raised error: " + subaction, e); if (e.getCritical()) throw e; else { actionHadErrors = true; nonCriticalErrors.add(new ActionError(e, subdata)); } } } // find forward behaviour ActionForwardPageIF tmpForward = actionGroup.getForwardPage(action, actionHadErrors); if (tmpForward != null) { // this is a principal action if (forwardPage != null) logger.error("More than one principal action found: " + param); else { forwardPage = tmpForward; logger.debug("Action " + action.getName() + " is principal; " + "request succeeded; found forward " + forwardPage); } } } // while // handle forward logic if (acresponse.getForward() != null) { // one of the actions set the forward; this then overrides // everything else forwardPage = new ActionForwardPage(acresponse.getForward(), acresponse.getParameters()); logger.debug("An action set the forward: " + forwardPage); } else if (forwardPage == null) { logger.debug("No forward page found; getting default"); forwardPage = actionGroup .getDefaultForwardPage(nonCriticalErrors.isEmpty() ? Constants.FORWARD_SUCCESS : Constants.FORWARD_FAILURE); } // commit transaction if (store != null) { store.commit(); logger.debug("Transaction committed"); } } // synchronized } catch (Throwable e) { logger.error("There was an exception during form submission", e); // rollback transaction if (store != null) { store.abort(); logger.debug("Transaction rolled back"); } if (e instanceof RuntimeException) throw (RuntimeException)e; else throw new OntopiaRuntimeException(e); } finally { // Remove the request data from the cache user.removeWorkingBundle(params.get(Constants.RP_REQUEST_ID)); // return topic map to navigator application if (navApp != null && topicmap != null) navApp.returnTopicMap(topicmap); // --- (C) Release variable from lock manager if (lockMan != null) { lockMan.unlock(user, lock_varname, false); } } // try: txn block // --- (D) Constructing the URL to forward to if (forwardPage == null) { if (nonCriticalErrors.isEmpty()) forwardPage = actionGroup .getDefaultForwardPage(Constants.FORWARD_SUCCESS); else forwardPage = actionGroup .getDefaultForwardPage(Constants.FORWARD_FAILURE); } logger.debug("Forward page is: " + forwardPage); if (!nonCriticalErrors.isEmpty()) request.getSession().setAttribute("nonCriticalErrors", nonCriticalErrors); // A default forward page is now REQUIRED in the actionConfig.dtd // So removed test for null forward page String url = forwardPage.getURL(); StringBuilder urlBuffer = new StringBuilder(url); urlBuffer.append('?'); // merge request parameters Map mergedParams = new HashMap(acresponse.getParameters()); mergedParams.putAll(forwardPage.getParameters()); // HACK: needed by ontopoly embedded request.setAttribute("oksResponseParams", mergedParams); request.setAttribute("oksForwardPage", url); // append request parameters urlBuffer.append(ReqParamUtils.params2URLQuery(mergedParams, params, charEnc)); // [finished building the URL of the 'forward' request] // --- (E) Apply request parameter rule to the request string String relativeURL = urlBuffer.toString(); logger.debug("Initial forward URL: " + relativeURL); String nextActionTemplate = forwardPage.getNextActionTemplate(); ParamRuleIF paramRule = forwardPage.getNextActionParamRule(); if ((nextActionTemplate != null && !nextActionTemplate.equals("")) || paramRule != null) { logger.debug("Applying param rule: " + paramRule); relativeURL = paramRule.generate(actionCtxt, null, nextActionTemplate, relativeURL); } // only let linkforward override if there are no errors if (nonCriticalErrors.isEmpty()) { String linkforward = params.get("linkforward"); if (linkforward != null && !linkforward.equals("")) { logger.debug("Changing relativeURL (" + relativeURL + ") to linkforward(" + linkforward + ")"); relativeURL = linkforward; } } Boolean embedded = (Boolean)request.getAttribute("ProcessServlet.embedded"); if (embedded == null || !embedded.booleanValue()) { logger.debug("Forward to " + relativeURL); response.sendRedirect(relativeURL); } else { logger.debug("In embedded mode, so no forward is done."); } } private String getCharacterEncoding(NavigatorApplicationIF navApp) { // ensure that request character encoding decoded correctly (bug #622) String charEnc = navApp.getConfiguration().getProperty( "defaultCharacterEncoding"); if (charEnc != null && charEnc.trim().equals("")) charEnc = null; return charEnc; } private UserIF getUser(HttpServletRequest request) throws UnavailableSessionException { // retrieve user object from session UserIF user = (UserIF) request.getSession().getAttribute( NavigatorApplicationIF.USER_KEY); if (user == null) throw new UnavailableSessionException("No user session available, " + "please log in (again)."); return user; } private Parameters getParameters(HttpServletRequest request, NavigatorApplicationIF navApp, String charenc) throws IOException, ServletException { // decode request into Parameters object Parameters params = ReqParamUtils.decodeParameters(request, charenc); return params; } private NavigatorApplicationIF getNavigatorApplication() { NavigatorApplicationIF navApp = NavigatorUtils .getNavigatorApplication(getServletContext()); if (navApp == null) logger.warn("NavigationApplication object is NOT available."); return navApp; } private void unlock(HttpServletRequest request, boolean forced) throws ServletException, IOException { UserIF user = getUser(request); NavigatorApplicationIF navApp = getNavigatorApplication(); Parameters params = getParameters(request, navApp, getCharacterEncoding(navApp)); String lock_varname = params.get(Constants.RP_LOCKVAR); if (lock_varname != null && !lock_varname.equals("")) { NamedLockManager lockMan = TagUtils .getNamedLockManager(getServletContext()); if (lockMan != null) { lockMan.unlock(user, lock_varname, forced); } } } // Internal methods private boolean changesMade(List actions) { Iterator it = actions.iterator(); while (it.hasNext()) { ActionData data = (ActionData) it.next(); ActionIF action = data.getAction().getAction(); if (action.getClass().equals(DummyAction.class) || action.getClass().equals(DefaultAction.class)) continue; return true; // a non-dummy action will be run } return false; // couldn't find a non-dummy action } private void logrequest(Parameters params) { Iterator it = params.getNames().iterator(); while (it.hasNext()) { String paramname = (String) it.next(); logger.debug("Param '" + paramname + "': '" + DebugUtils.toString(params.getValues(paramname)) + "'"); } } private void errorLogrequest(Parameters params, String indentation) { Iterator it = params.getNames().iterator(); while (it.hasNext()) { String paramname = (String) it.next(); logger.error(indentation + "Param '" + paramname + "': '" + DebugUtils.toString(params.getValues(paramname)) + "'"); } } private boolean isValueEqual(String param, String[] values, Set value) { // value can be: // - AlwaysDifferentObject (meaning, say no, and run the action) // - Set String (checkbox) // - Set String (list) // - Set String (field) // - Set null (button; will be different if button were pressed) Set current = new HashSet(); if (values == null) current.add(null); else for (int ix = 0; ix < values.length; ix++) current.add(values[ix]); boolean result = value.equals(current); logger.debug("Action " + param + " had value " + value + "; now " + current + (result ? "; will not run" : "; will run")); return result; } // 'actions' list contains ActionData // actionGroup knows the correct order of the ActionIFs in the ActionData // returns list of ActionData private List sortActions(List actions, ActionGroupIF actionGroup) { List sorted = new ArrayList(); Iterator it = actionGroup.getActions().iterator(); while (it.hasNext()) { ActionInGroup action = (ActionInGroup) it.next(); Iterator it2 = actions.iterator(); while (it2.hasNext()) { ActionData data = (ActionData) it2.next(); if (action.equals(data.getAction())) sorted.add(data); } } return sorted; } }