001/* 002 * Copyright 2012 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.ui.dispatcher; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.util.ArrayList; 021import java.util.Enumeration; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Map.Entry; 026 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.cocoon.ProcessingException; 030import org.apache.cocoon.ResourceNotFoundException; 031import org.apache.cocoon.environment.ObjectModelHelper; 032import org.apache.cocoon.environment.Request; 033import org.apache.cocoon.generation.ServiceableGenerator; 034import org.apache.cocoon.util.location.LocatedException; 035import org.apache.cocoon.xml.AttributesImpl; 036import org.apache.cocoon.xml.XMLUtils; 037import org.apache.commons.io.IOUtils; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.commons.lang3.exception.ExceptionUtils; 040import org.apache.excalibur.source.Source; 041import org.apache.excalibur.source.SourceResolver; 042import org.apache.excalibur.xml.sax.SAXParser; 043import org.xml.sax.Attributes; 044import org.xml.sax.ContentHandler; 045import org.xml.sax.InputSource; 046import org.xml.sax.SAXException; 047 048import org.ametys.core.authentication.AuthenticateAction; 049import org.ametys.core.util.IgnoreRootHandler; 050import org.ametys.core.util.JSONUtils; 051import org.ametys.core.util.URIUtils; 052import org.ametys.plugins.core.ui.util.RequestAttributesHelper; 053import org.ametys.runtime.workspace.WorkspaceMatcher; 054 055/** 056 * This generator read the request incoming from the client org.ametys.servercomm.ServerComm component, 057 * then dispatch it to given url 058 * and aggregate the result 059 */ 060public class DispatchGenerator extends ServiceableGenerator 061{ 062 /** Request Attributes Helper */ 063 protected RequestAttributesHelper _requestAttributesHelper; 064 065 private SourceResolver _resolver; 066 private DispatchProcessExtensionPoint _dispatchProcessExtensionPoint; 067 private JSONUtils _jsonUtils; 068 069 @Override 070 public void service(ServiceManager smanager) throws ServiceException 071 { 072 super.service(smanager); 073 074 _resolver = (SourceResolver) smanager.lookup(org.apache.excalibur.source.SourceResolver.ROLE); 075 _dispatchProcessExtensionPoint = (DispatchProcessExtensionPoint) manager.lookup(DispatchProcessExtensionPoint.ROLE); 076 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 077 _requestAttributesHelper = (RequestAttributesHelper) manager.lookup(RequestAttributesHelper.ROLE); 078 } 079 080 @Override 081 public void generate() throws IOException, SAXException, ProcessingException 082 { 083 // Put the header for the client side ServerComm to ensure the authentication was correctly handled 084 ObjectModelHelper.getResponse(objectModel).addHeader("Ametys-Dispatched", "true"); 085 086 String parametersAsJSONString = _getRequestBody(); 087 Map<String, Object> parametersAsMap = _jsonUtils.convertJsonToMap(parametersAsJSONString); 088 089 String contextAsJSONString = _getRequestContext(); 090 Map<String, Object> contextAsMap = _jsonUtils.convertJsonToMap(contextAsJSONString); 091 092 contentHandler.startDocument(); 093 XMLUtils.startElement(contentHandler, "responses"); 094 095 _dispatching(parametersAsMap, contextAsMap); 096 097 XMLUtils.endElement(contentHandler, "responses"); 098 contentHandler.endDocument(); 099 } 100 101 private String _getRequestBody() 102 { 103 return ObjectModelHelper.getRequest(objectModel).getParameter("content"); 104 } 105 106 private String _getRequestContext() 107 { 108 return ObjectModelHelper.getRequest(objectModel).getParameter("context.parameters"); 109 } 110 111 @SuppressWarnings("unchecked") 112 private void _dispatching(Map<String, Object> parametersAsMap, Map<String, Object> contextAsMap) throws SAXException 113 { 114 Map<String, Object> attributes = _requestAttributesHelper.saveRequestAttributes(); 115 contextAsMap.putAll(transmitAttributes(attributes)); 116 117 Map<String, Long> times = new HashMap<>(); 118 for (String parameterKey : parametersAsMap.keySet()) 119 { 120 long t0 = System.currentTimeMillis(); 121 122 _requestAttributesHelper.removeRequestAttributes(); 123 124 for (String extension : _dispatchProcessExtensionPoint.getExtensionsIds()) 125 { 126 DispatchRequestProcess processor = _dispatchProcessExtensionPoint.getExtension(extension); 127 processor.preProcess(ObjectModelHelper.getRequest(objectModel)); 128 } 129 130 _setContextInRequestAttributes(contextAsMap); 131 132 Map<String, Object> parameterObject = (Map<String, Object>) parametersAsMap.get(parameterKey); 133 134 String pluginOrWorkspace = (String) parameterObject.get("pluginOrWorkspace"); 135 String relativeUrl = (String) parameterObject.get("url"); 136 String responseType = (String) parameterObject.get("responseType"); 137 138 Map<String, Object> requestParameters = (Map<String, Object>) parameterObject.get("parameters"); 139 _replaceFiles(requestParameters, parameterKey); 140 141 Source response = null; 142 143 ResponseHandler responseHandler = null; 144 try 145 { 146 String url = _createUrl(pluginOrWorkspace, relativeUrl, requestParameters != null ? requestParameters : new HashMap<String, Object>()); 147 148 if (getLogger().isInfoEnabled()) 149 { 150 getLogger().info("Dispatching url '" + url + "'"); 151 } 152 153 response = _resolver.resolveURI(url, null, requestParameters); 154 155 responseHandler = new ResponseHandler(contentHandler, parameterKey, "200"); 156 157 try (InputStream is = response.getInputStream()) 158 { 159 if ("xml".equalsIgnoreCase(responseType)) 160 { 161 SAXParser saxParser = null; 162 try 163 { 164 // DO NOT USE SitemapSource.toSAX in this case 165 saxParser = (SAXParser) manager.lookup(SAXParser.ROLE); 166 saxParser.parse(new InputSource(is), responseHandler); 167 } 168 catch (ServiceException e) 169 { 170 throw new ProcessingException("Unable to get a SAX parser", e); 171 } 172 finally 173 { 174 manager.release(saxParser); 175 } 176 } 177 else 178 { 179 responseHandler.startDocument(); 180 181 String data = IOUtils.toString(is, "UTF-8"); 182 if ("xml2text".equalsIgnoreCase(responseType)) 183 { 184 // removing xml prolog and surrounding 'text' tag 185 data = data.substring(data.indexOf(">", data.indexOf("?>") + 2) + 1, data.lastIndexOf("<")); 186 } 187 XMLUtils.data(responseHandler, data); 188 189 responseHandler.endDocument(); 190 } 191 } 192 } 193 catch (Throwable e) 194 { 195 String message = String.format("Can not dispatch request '%s' : '%s' '%s' '%s'", parameterKey , pluginOrWorkspace, relativeUrl, requestParameters); 196 197 // Ensure SAXException are unrolled the right way 198 getLogger().error(message, new LocatedException(message, e)); 199 200 // Makes the output xml ok 201 if (responseHandler != null) 202 { 203 responseHandler.exceptionFinish(); 204 } 205 206 Throwable t = _unroll(e); 207 208 String code = "500"; 209 if (t instanceof ResourceNotFoundException || t.toString().startsWith("org.apache.cocoon.ResourceNotFoundException:")) 210 { 211 code = "404"; 212 } 213 214 AttributesImpl attrs = new AttributesImpl(); 215 attrs.addCDATAAttribute("id", parameterKey); 216 attrs.addCDATAAttribute("code", code); 217 218 String exceptionMessage = t.getMessage(); 219 220 XMLUtils.startElement(contentHandler, "response", attrs); 221 XMLUtils.createElement(contentHandler, "message", _escape(exceptionMessage != null ? exceptionMessage : "")); 222 XMLUtils.createElement(contentHandler, "stacktrace", _escape(ExceptionUtils.getStackTrace(t))); 223 XMLUtils.endElement(contentHandler, "response"); 224 } 225 finally 226 { 227 _resolver.release(response); 228 229 for (String extension : _dispatchProcessExtensionPoint.getExtensionsIds()) 230 { 231 DispatchRequestProcess processor = _dispatchProcessExtensionPoint.getExtension(extension); 232 processor.postProcess(ObjectModelHelper.getRequest(objectModel)); 233 } 234 } 235 236 long t1 = System.currentTimeMillis(); 237 long time = t1 - t0; 238 times.put(parameterKey, time); 239 getLogger().debug("Request '" + parameterKey + "' took " + time + "ms"); 240 } 241 242 _requestAttributesHelper.restoreRequestAttributes(attributes); 243 244 // Send measured times 245 AttributesImpl attrs = new AttributesImpl(); 246 attrs.addCDATAAttribute("duration", Long.toString(times.values().stream().mapToLong(Long::longValue).sum())); 247 XMLUtils.startElement(contentHandler, "times", attrs); 248 for (String parameterKey: times.keySet()) 249 { 250 AttributesImpl internalAttrs = new AttributesImpl(); 251 internalAttrs.addCDATAAttribute("id", parameterKey); 252 internalAttrs.addCDATAAttribute("duration", Long.toString(times.get(parameterKey))); 253 XMLUtils.createElement(contentHandler, "time", internalAttrs); 254 } 255 XMLUtils.endElement(contentHandler, "times"); 256 } 257 258 private void _replaceFiles(Map<String, Object> params, String index) 259 { 260 Map<String, Object> uploadedFiles = _extractFiles(index); 261 if (!uploadedFiles.isEmpty()) 262 { 263 for (Entry<String, Object> entry : uploadedFiles.entrySet()) 264 { 265 Object parent = params; 266 267 String[] cursors = StringUtils.split(entry.getKey(), "."); 268 for (int i = 0; i < cursors.length; i++) 269 { 270 String cursor = cursors[i]; 271 272 if (parent instanceof Map) 273 { 274 @SuppressWarnings("unchecked") 275 Map<String, Object> parentMap = (Map) parent; 276 277 if (i == cursors.length - 1) 278 { 279 parentMap.put(cursor, entry.getValue()); 280 } 281 else 282 { 283 parent = parentMap.get(cursor); 284 } 285 } 286 else if (parent instanceof List) 287 { 288 @SuppressWarnings("unchecked") 289 List<Object> parentList = (List) parent; 290 int cursorIndex = Integer.parseInt(cursor); 291 292 if (i == cursors.length - 1) 293 { 294 parentList.remove(cursorIndex); 295 parentList.add(cursorIndex, entry.getValue()); 296 297 } 298 else 299 { 300 parent = parentList.get(cursorIndex); 301 } 302 } 303 else 304 { 305 getLogger().warn("Cannot replace files in objects that are not java.util.List nor java.util.Map: " + parent.getClass().getName()); 306 break; 307 } 308 } 309 } 310 } 311 } 312 313 private Map<String, Object> _extractFiles(String index) 314 { 315 Request request = ObjectModelHelper.getRequest(objectModel); 316 317 Map<String, Object> files = new HashMap<>(); 318 319 // Extract the optional list of uploaded files 320 Enumeration paramNames = request.getParameterNames(); 321 while (paramNames.hasMoreElements()) 322 { 323 String paramName = (String) paramNames.nextElement(); 324 if (paramName.startsWith("request#" + index + "#")) 325 { 326 files.put(StringUtils.removeStart(paramName, "request#" + index + "#"), request.get(paramName)); 327 } 328 } 329 330 return files; 331 } 332 333 334 /** 335 * Filters attributes that should be transmitted to the dispatched request 336 * @param attributes The full list of attributes 337 * @return The attributes filtered 338 */ 339 protected Map<String, Object> transmitAttributes(Map<String, Object> attributes) 340 { 341 Map<String, Object> contextAsMap = new HashMap<>(); 342 343 String[] attributesToTransmit = new String[] 344 { 345 AuthenticateAction.REQUEST_ATTRIBUTE_AUTHENTICATED, 346 WorkspaceMatcher.IN_WORKSPACE_URL, 347 WorkspaceMatcher.WORKSPACE_NAME, 348 WorkspaceMatcher.WORKSPACE_THEME, 349 WorkspaceMatcher.WORKSPACE_THEME_URL, 350 WorkspaceMatcher.WORKSPACE_URI 351 }; 352 353 for (String attributeToTransmit : attributesToTransmit) 354 { 355 contextAsMap.put(attributeToTransmit, attributes.get(attributeToTransmit)); 356 } 357 358 return contextAsMap; 359 } 360 361 private Throwable _unroll(Throwable initial) 362 { 363 Throwable t = initial; 364 while (t.getCause() != null || t instanceof SAXException && ((SAXException) t).getException() != null) 365 { 366 if (t instanceof SAXException) 367 { 368 t = ((SAXException) t).getException(); 369 } 370 else 371 { 372 t = t.getCause(); 373 } 374 } 375 376 return t; 377 } 378 379 /** 380 * Set the request attributes from the contextual parameters 381 * @param contextAsMap The contextual parameters 382 */ 383 protected void _setContextInRequestAttributes(Map<String, Object> contextAsMap) 384 { 385 if (contextAsMap != null) 386 { 387 Request request = ObjectModelHelper.getRequest(objectModel); 388 389 for (String name : contextAsMap.keySet()) 390 { 391 request.setAttribute(name, contextAsMap.get(name)); 392 } 393 } 394 } 395 396 private String _escape(String value) 397 { 398 return value.replaceAll("&", "&").replaceAll("<", "<".replaceAll(">", ">")); 399 } 400 401 /** 402 * Create url to call 403 * @param pluginOrWorkspace the plugin or workspace name 404 * @param relativeUrl the relative url 405 * @param requestParameters the request parameters. Can not be null. 406 * @return the full url 407 */ 408 @SuppressWarnings("unchecked") 409 protected String _createUrl(String pluginOrWorkspace, String relativeUrl, Map<String, Object> requestParameters) 410 { 411 StringBuilder url = new StringBuilder(); 412 413 String urlPrefix = _getUrlPrefix(pluginOrWorkspace); 414 url.append(urlPrefix); 415 416 url.append(_getRelativePath(relativeUrl)); 417 418 int queryIndex = relativeUrl.indexOf("?"); 419 420 if (queryIndex == -1 && !requestParameters.isEmpty()) 421 { 422 // no existing parameters in request 423 url.append("?"); 424 425 for (String key : requestParameters.keySet()) 426 { 427 Object value = requestParameters.get(key); 428 if (value instanceof List) 429 { 430 List<Object> valueAsList = (List<Object>) value; 431 for (Object v : valueAsList) 432 { 433 if (v != null) 434 { 435 url.append(_buildQueryParameter(key, v)); 436 } 437 } 438 } 439 else if (value != null) 440 { 441 url.append(_buildQueryParameter(key, value)); 442 } 443 } 444 } 445 else if (queryIndex > 0) 446 { 447 url.append("?"); 448 449 String queryUrl = relativeUrl.substring(queryIndex + 1, relativeUrl.length()); 450 String[] queryParameters = queryUrl.split("&"); 451 452 for (String queryParameter : queryParameters) 453 { 454 if (StringUtils.isNotBlank(queryParameter)) 455 { 456 String[] part = queryParameter.split("="); 457 String key = part[0]; 458 String v = part.length > 1 ? part[1] : ""; 459 String value = URIUtils.decode(v); 460 url.append(_buildQueryParameter(key, value)); 461 462 if (!requestParameters.containsKey(key)) 463 { 464 requestParameters.put(key, value); 465 } 466 } 467 } 468 } 469 470 return url.toString(); 471 } 472 473 private String _getRelativePath(String url) 474 { 475 int beginIndex = url.length() != 0 && url.charAt(0) == '/' ? 1 : 0; 476 int endIndex = url.indexOf("?"); 477 return endIndex == -1 ? url.substring(beginIndex) : url.substring(beginIndex, endIndex); 478 } 479 480 private StringBuilder _buildQueryParameter(String key, Object value) 481 { 482 StringBuilder queryParameter = new StringBuilder(); 483 queryParameter.append(key); 484 queryParameter.append("="); 485 queryParameter.append(String.valueOf(value).replaceAll("%", "%25").replaceAll("=", "%3D").replaceAll("&", "%26").replaceAll("\\+", "%2B")); 486 queryParameter.append("&"); 487 488 return queryParameter; 489 } 490 491 /** 492 * Get the url prefix 493 * @param pluginOrWorkspace the plugin or workspace name 494 * @return the url prefix 495 */ 496 protected String _getUrlPrefix (String pluginOrWorkspace) 497 { 498 StringBuffer url = new StringBuffer("cocoon://"); 499 if (pluginOrWorkspace != null && !pluginOrWorkspace.startsWith("_")) 500 { 501 url.append("_plugins/"); 502 url.append(pluginOrWorkspace); 503 url.append("/"); 504 } 505 else if (pluginOrWorkspace != null) 506 { 507 url.append(pluginOrWorkspace); 508 url.append("/"); 509 } 510 511 return url.toString(); 512 } 513 514 /** 515 * Wrap the handler ignore start and end document, but adding a response tag. 516 */ 517 public static class ResponseHandler extends IgnoreRootHandler 518 { 519 private final String _parameterKey; 520 private final ContentHandler _handler; 521 private final String _code; 522 523 private final List<String> _startedElements; 524 525 /** 526 * Create the wrapper 527 * @param handler The content handler to wrap 528 * @param parameterKey The id of the response 529 * @param code The status code of the response 530 */ 531 public ResponseHandler(ContentHandler handler, String parameterKey, String code) 532 { 533 super(handler); 534 _handler = handler; 535 _parameterKey = parameterKey; 536 _code = code; 537 _startedElements = new ArrayList<>(); 538 } 539 540 /** 541 * Finish abruptly this handler to obtain a correct XML 542 * @throws SAXException if an error occurred 543 */ 544 public void exceptionFinish() throws SAXException 545 { 546 while (_startedElements.size() > 0) 547 { 548 XMLUtils.endElement(_handler, _startedElements.get(_startedElements.size() - 1)); 549 _startedElements.remove(_startedElements.size() - 1); 550 } 551 } 552 553 @Override 554 public void startDocument() throws SAXException 555 { 556 super.startDocument(); 557 558 AttributesImpl attrs = new AttributesImpl(); 559 attrs.addCDATAAttribute("id", _parameterKey); 560 attrs.addCDATAAttribute("code", _code); 561 XMLUtils.startElement(_handler, "response", attrs); 562 563 _startedElements.add("response"); 564 } 565 566 @Override 567 public void startElement(String uri, String loc, String raw, Attributes a) throws SAXException 568 { 569 super.startElement(uri, loc, raw, a); 570 _startedElements.add(loc); 571 } 572 573 @Override 574 public void endElement(String uri, String loc, String raw) throws SAXException 575 { 576 super.endElement(uri, loc, raw); 577 578 if (!StringUtils.equals(_startedElements.get(_startedElements.size() - 1), loc)) 579 { 580 throw new SAXException("Sax events are not consistents. Cannot close <" + loc + "> while it should be <" + _startedElements.get(_startedElements.size() - 1) + ">"); 581 } 582 583 _startedElements.remove(_startedElements.size() - 1); 584 } 585 586 @Override 587 public void endDocument() throws SAXException 588 { 589 XMLUtils.endElement(_handler, "response"); 590 591 if (_startedElements.size() != 1) 592 { 593 throw new SAXException("Sax events are not consistents. Remaining " + _startedElements.size() + " events (should be one)."); 594 } 595 _startedElements.remove(_startedElements.size() - 1); 596 super.endDocument(); 597 } 598 } 599}