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.io.UnsupportedEncodingException; 021import java.net.URLDecoder; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.Enumeration; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.cocoon.ProcessingException; 032import org.apache.cocoon.ResourceNotFoundException; 033import org.apache.cocoon.environment.ObjectModelHelper; 034import org.apache.cocoon.environment.Request; 035import org.apache.cocoon.generation.ServiceableGenerator; 036import org.apache.cocoon.util.location.LocatedException; 037import org.apache.cocoon.xml.AttributesImpl; 038import org.apache.cocoon.xml.XMLUtils; 039import org.apache.commons.io.IOUtils; 040import org.apache.commons.lang.StringUtils; 041import org.apache.commons.lang.exception.ExceptionUtils; 042import org.apache.excalibur.source.Source; 043import org.apache.excalibur.source.SourceResolver; 044import org.apache.excalibur.xml.sax.SAXParser; 045import org.xml.sax.Attributes; 046import org.xml.sax.ContentHandler; 047import org.xml.sax.InputSource; 048import org.xml.sax.SAXException; 049 050import org.ametys.core.authentication.AuthenticateAction; 051import org.ametys.core.right.RightManager; 052import org.ametys.core.util.IgnoreRootHandler; 053import org.ametys.core.util.JSONUtils; 054import org.ametys.runtime.workspace.WorkspaceMatcher; 055 056/** 057 * This generator read the request incoming from the client org.ametys.servercomm.ServerComm component, 058 * then dispatch it to given url 059 * and aggregate the result 060 */ 061public class DispatchGenerator extends ServiceableGenerator 062{ 063 private SourceResolver _resolver; 064 private SAXParser _saxParser; 065 private DispatchProcessExtensionPoint _dispatchProcessExtensionPoint; 066 private JSONUtils _jsonUtils; 067 068 @Override 069 public void service(ServiceManager smanager) throws ServiceException 070 { 071 super.service(smanager); 072 073 _resolver = (SourceResolver) smanager.lookup(org.apache.excalibur.source.SourceResolver.ROLE); 074 _saxParser = (SAXParser) manager.lookup(SAXParser.ROLE); 075 _dispatchProcessExtensionPoint = (DispatchProcessExtensionPoint) manager.lookup(DispatchProcessExtensionPoint.ROLE); 076 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 077 } 078 079 @Override 080 public void generate() throws IOException, SAXException, ProcessingException 081 { 082 String parametersAsJSONString = _getRequestBody(); 083 Map<String, Object> parametersAsMap = _jsonUtils.convertJsonToMap(parametersAsJSONString); 084 085 String contextAsJSONString = _getRequestContext(); 086 Map<String, Object> contextAsMap = _jsonUtils.convertJsonToMap(contextAsJSONString); 087 088 contentHandler.startDocument(); 089 XMLUtils.startElement(contentHandler, "responses"); 090 091 _dispatching(parametersAsMap, contextAsMap); 092 093 XMLUtils.endElement(contentHandler, "responses"); 094 contentHandler.endDocument(); 095 } 096 097 private String _getRequestBody() 098 { 099 return ObjectModelHelper.getRequest(objectModel).getParameter("content"); 100 } 101 102 private String _getRequestContext() 103 { 104 return ObjectModelHelper.getRequest(objectModel).getParameter("context.parameters"); 105 } 106 107 @SuppressWarnings("unchecked") 108 private void _dispatching(Map<String, Object> parametersAsMap, Map<String, Object> contextAsMap) throws SAXException 109 { 110 Map<String, Object> attributes = _saveRequestAttributes(); 111 contextAsMap.putAll(transmitAttributes(attributes)); 112 113 for (String parameterKey : parametersAsMap.keySet()) 114 { 115 long t0 = System.currentTimeMillis(); 116 117 _removeRequestAttributes(); 118 119 for (String extension : _dispatchProcessExtensionPoint.getExtensionsIds()) 120 { 121 DispatchRequestProcess processor = _dispatchProcessExtensionPoint.getExtension(extension); 122 processor.preProcess(ObjectModelHelper.getRequest(objectModel)); 123 } 124 125 _setContextInRequestAttributes(contextAsMap); 126 127 Map<String, Object> parameterObject = (Map<String, Object>) parametersAsMap.get(parameterKey); 128 129 String pluginOrWorkspace = (String) parameterObject.get("pluginOrWorkspace"); 130 String relativeUrl = (String) parameterObject.get("url"); 131 String responseType = (String) parameterObject.get("responseType"); 132 133 Map<String, Object> requestParameters = (Map<String, Object>) parameterObject.get("parameters"); 134 135 Source response = null; 136 137 ResponseHandler responseHandler = null; 138 try 139 { 140 String url = _createUrl(pluginOrWorkspace, relativeUrl, requestParameters != null ? requestParameters : new HashMap<String, Object>()); 141 142 if (getLogger().isInfoEnabled()) 143 { 144 getLogger().info("Dispatching url '" + url + "'"); 145 } 146 147 response = _resolver.resolveURI(url, null, requestParameters); 148 149 responseHandler = new ResponseHandler(contentHandler, parameterKey, "200", t0); 150 151 try (InputStream is = response.getInputStream()) 152 { 153 if ("xml".equalsIgnoreCase(responseType)) 154 { 155 // DO NOT USE SitemapSource.toSAX in this case 156 _saxParser.parse(new InputSource(is), responseHandler); 157 } 158 else 159 { 160 responseHandler.startDocument(); 161 162 String data = IOUtils.toString(is, "UTF-8"); 163 if ("xml2text".equalsIgnoreCase(responseType)) 164 { 165 // removing xml prolog and surrounding 'text' tag 166 data = data.substring(data.indexOf(">", data.indexOf("?>") + 2) + 1, data.lastIndexOf("<")); 167 } 168 XMLUtils.data(responseHandler, data); 169 170 responseHandler.endDocument(); 171 } 172 } 173 } 174 catch (Throwable e) 175 { 176 String message = String.format("Can not dispatch request '%s' : '%s' '%s' '%s'", parameterKey , pluginOrWorkspace, relativeUrl, requestParameters); 177 178 // Ensure SAXException are unrolled the right way 179 getLogger().error(message, new LocatedException(message, e)); 180 181 // Makes the output xml ok 182 if (responseHandler != null) 183 { 184 responseHandler.exceptionFinish(); 185 } 186 187 Throwable t = _unroll(e); 188 189 String code = "500"; 190 if (t instanceof ResourceNotFoundException || t.toString().startsWith("org.apache.cocoon.ResourceNotFoundException:")) 191 { 192 code = "404"; 193 } 194 195 long t1 = System.currentTimeMillis(); 196 197 AttributesImpl attrs = new AttributesImpl(); 198 attrs.addCDATAAttribute("id", parameterKey); 199 attrs.addCDATAAttribute("code", code); 200 attrs.addCDATAAttribute("duration", Long.toString(t1 - t0)); 201 202 String exceptionMessage = t.getMessage(); 203 204 XMLUtils.startElement(contentHandler, "response", attrs); 205 XMLUtils.createElement(contentHandler, "message", _escape(exceptionMessage != null ? exceptionMessage : "")); 206 XMLUtils.createElement(contentHandler, "stacktrace", _escape(ExceptionUtils.getFullStackTrace(t))); 207 XMLUtils.endElement(contentHandler, "response"); 208 } 209 finally 210 { 211 _resolver.release(response); 212 213 for (String extension : _dispatchProcessExtensionPoint.getExtensionsIds()) 214 { 215 DispatchRequestProcess processor = _dispatchProcessExtensionPoint.getExtension(extension); 216 processor.postProcess(ObjectModelHelper.getRequest(objectModel)); 217 } 218 } 219 } 220 221 _restoreRequestAttributes(attributes); 222 } 223 224 /** 225 * Filters attributes that should be transmitted to the dispatched request 226 * @param attributes The full list of attributes 227 * @return The attributes filtered 228 */ 229 protected Map<String, Object> transmitAttributes(Map<String, Object> attributes) 230 { 231 Map<String, Object> contextAsMap = new HashMap<>(); 232 233 String[] attributesToTransmit = new String[] 234 { 235 AuthenticateAction.REQUEST_ATTRIBUTE_AUTHENTICATED, 236 WorkspaceMatcher.IN_WORKSPACE_URL, 237 WorkspaceMatcher.WORKSPACE_NAME, 238 WorkspaceMatcher.WORKSPACE_THEME, 239 WorkspaceMatcher.WORKSPACE_THEME_URL, 240 WorkspaceMatcher.WORKSPACE_URI, 241 RightManager.CACHE_REQUEST_ATTRIBUTE_NAME 242 }; 243 244 for (String attributeToTransmit : attributesToTransmit) 245 { 246 contextAsMap.put(attributeToTransmit, attributes.get(attributeToTransmit)); 247 } 248 249 return contextAsMap; 250 } 251 252 private Throwable _unroll(Throwable initial) 253 { 254 Throwable t = initial; 255 while (t.getCause() != null || t instanceof SAXException && ((SAXException) t).getException() != null) 256 { 257 if (t instanceof SAXException) 258 { 259 t = ((SAXException) t).getException(); 260 } 261 else 262 { 263 t = t.getCause(); 264 } 265 } 266 267 return t; 268 } 269 270 /** 271 * Clean the requests attributes and add those in the map 272 * @param attributes The attributes to restore 273 */ 274 private void _restoreRequestAttributes(Map<String, Object> attributes) 275 { 276 _removeRequestAttributes(); 277 278 Request request = ObjectModelHelper.getRequest(objectModel); 279 for (String attrName : attributes.keySet()) 280 { 281 request.setAttribute(attrName, attributes.get(attrName)); 282 } 283 } 284 285 @SuppressWarnings("unchecked") 286 private void _removeRequestAttributes() 287 { 288 Request request = ObjectModelHelper.getRequest(objectModel); 289 290 List<String> attrNames = Collections.list(request.getAttributeNames()); 291 for (String attrName : attrNames) 292 { 293 request.removeAttribute(attrName); 294 } 295 } 296 297 private void _setContextInRequestAttributes(Map<String, Object> contextAsMap) 298 { 299 if (contextAsMap != null) 300 { 301 Request request = ObjectModelHelper.getRequest(objectModel); 302 303 for (String name : contextAsMap.keySet()) 304 { 305 request.setAttribute(name, contextAsMap.get(name)); 306 } 307 } 308 } 309 310 /** 311 * Transforms the request attributes into a map and clean the attributes 312 * @return A copy of all the request attributes 313 */ 314 private Map<String, Object> _saveRequestAttributes() 315 { 316 Map<String, Object> attrs = new HashMap<>(); 317 318 Request request = ObjectModelHelper.getRequest(objectModel); 319 320 request.setAttribute(RightManager.CACHE_REQUEST_ATTRIBUTE_NAME, new HashMap<>()); 321 322 Enumeration<String> attrNames = request.getAttributeNames(); 323 while (attrNames.hasMoreElements()) 324 { 325 String attrName = attrNames.nextElement(); 326 Object value = request.getAttribute(attrName); 327 328 attrs.put(attrName, value); 329 } 330 331 return attrs; 332 } 333 334 private String _escape(String value) 335 { 336 return value.replaceAll("&", "&").replaceAll("<", "<".replaceAll(">", ">")); 337 } 338 339 /** 340 * Create url to call 341 * @param pluginOrWorkspace the plugin or workspace name 342 * @param relativeUrl the relative url 343 * @param requestParameters the request parameters. Can not be null. 344 * @return the full url 345 */ 346 @SuppressWarnings("unchecked") 347 protected String _createUrl(String pluginOrWorkspace, String relativeUrl, Map<String, Object> requestParameters) 348 { 349 StringBuilder url = new StringBuilder(); 350 351 String urlPrefix = _getUrlPrefix(pluginOrWorkspace); 352 url.append(urlPrefix); 353 354 url.append(_getRelativePath(relativeUrl)); 355 356 int queryIndex = relativeUrl.indexOf("?"); 357 358 if (queryIndex == -1 && !requestParameters.isEmpty()) 359 { 360 // no existing parameters in request 361 url.append("?"); 362 363 for (String key : requestParameters.keySet()) 364 { 365 Object value = requestParameters.get(key); 366 if (value instanceof List) 367 { 368 List<Object> valueAsList = (List<Object>) value; 369 for (Object v : valueAsList) 370 { 371 if (v != null) 372 { 373 url.append(_buildQueryParameter(key, v)); 374 } 375 } 376 } 377 else if (value != null) 378 { 379 url.append(_buildQueryParameter(key, value)); 380 } 381 } 382 } 383 else if (queryIndex > 0) 384 { 385 url.append("?"); 386 387 String queryUrl = relativeUrl.substring(queryIndex + 1, relativeUrl.length()); 388 String[] queryParameters = queryUrl.split("&"); 389 390 for (String queryParameter : queryParameters) 391 { 392 if (StringUtils.isNotBlank(queryParameter)) 393 { 394 String[] part = queryParameter.split("="); 395 String key = part[0]; 396 String v = part.length > 1 ? part[1] : ""; 397 try 398 { 399 String value = URLDecoder.decode(v, "UTF-8"); 400 url.append(_buildQueryParameter(key, value)); 401 402 if (!requestParameters.containsKey(key)) 403 { 404 requestParameters.put(key, value); 405 } 406 } 407 catch (UnsupportedEncodingException e) 408 { 409 getLogger().error("Unsupported encoding for request parameter '" + key + "' and value '" + v + "'", e); 410 } 411 } 412 } 413 } 414 415 return url.toString(); 416 } 417 418 private String _getRelativePath(String url) 419 { 420 int beginIndex = url.length() != 0 && url.charAt(0) == '/' ? 1 : 0; 421 int endIndex = url.indexOf("?"); 422 return endIndex == -1 ? url.substring(beginIndex) : url.substring(beginIndex, endIndex); 423 } 424 425 private StringBuilder _buildQueryParameter(String key, Object value) 426 { 427 StringBuilder queryParameter = new StringBuilder(); 428 queryParameter.append(key); 429 queryParameter.append("="); 430 queryParameter.append(String.valueOf(value).replaceAll("%", "%25").replaceAll("=", "%3D").replaceAll("&", "%26").replaceAll("\\+", "%2B")); 431 queryParameter.append("&"); 432 433 return queryParameter; 434 } 435 436 /** 437 * Get the url prefix 438 * @param pluginOrWorkspace the plugin or workspace name 439 * @return the url prefix 440 */ 441 protected String _getUrlPrefix (String pluginOrWorkspace) 442 { 443 StringBuffer url = new StringBuffer("cocoon://"); 444 if (pluginOrWorkspace != null && !pluginOrWorkspace.startsWith("_")) 445 { 446 url.append("_plugins/"); 447 url.append(pluginOrWorkspace); 448 url.append("/"); 449 } 450 else if (pluginOrWorkspace != null) 451 { 452 url.append(pluginOrWorkspace); 453 url.append("/"); 454 } 455 456 return url.toString(); 457 } 458 459 /** 460 * Wrap the handler ignore start and end document, but adding a response tag. 461 */ 462 public static class ResponseHandler extends IgnoreRootHandler 463 { 464 private final String _parameterKey; 465 private final ContentHandler _handler; 466 private final String _code; 467 private long _t0; 468 469 private final List<String> _startedElements; 470 471 /** 472 * Create the wrapper 473 * @param handler The content handler to wrap 474 * @param parameterKey The id of the response 475 * @param code The status code of the response 476 * @param t0 System time when the process started 477 */ 478 public ResponseHandler(ContentHandler handler, String parameterKey, String code, long t0) 479 { 480 super(handler); 481 _handler = handler; 482 _parameterKey = parameterKey; 483 _code = code; 484 _t0 = t0; 485 _startedElements = new ArrayList<>(); 486 } 487 488 /** 489 * Finish abruptly this handler to obtain a correct XML 490 * @throws SAXException if an error occurred 491 */ 492 public void exceptionFinish() throws SAXException 493 { 494 while (_startedElements.size() > 0) 495 { 496 XMLUtils.endElement(_handler, _startedElements.get(_startedElements.size() - 1)); 497 _startedElements.remove(_startedElements.size() - 1); 498 } 499 } 500 501 @Override 502 public void startDocument() throws SAXException 503 { 504 super.startDocument(); 505 506 long t1 = System.currentTimeMillis(); 507 508 AttributesImpl attrs = new AttributesImpl(); 509 attrs.addCDATAAttribute("id", _parameterKey); 510 attrs.addCDATAAttribute("code", _code); 511 attrs.addCDATAAttribute("duration", Long.toString(t1 - _t0)); 512 XMLUtils.startElement(_handler, "response", attrs); 513 514 _startedElements.add("response"); 515 } 516 517 @Override 518 public void startElement(String uri, String loc, String raw, Attributes a) throws SAXException 519 { 520 super.startElement(uri, loc, raw, a); 521 _startedElements.add(loc); 522 } 523 524 @Override 525 public void endElement(String uri, String loc, String raw) throws SAXException 526 { 527 super.endElement(uri, loc, raw); 528 529 if (!StringUtils.equals(_startedElements.get(_startedElements.size() - 1), loc)) 530 { 531 throw new SAXException("Sax events are not consistents. Cannot close <" + loc + "> while it should be <" + _startedElements.get(_startedElements.size() - 1) + ">"); 532 } 533 534 _startedElements.remove(_startedElements.size() - 1); 535 } 536 537 @Override 538 public void endDocument() throws SAXException 539 { 540 XMLUtils.endElement(_handler, "response"); 541 542 if (_startedElements.size() != 1) 543 { 544 throw new SAXException("Sax events are not consistents. Remaining " + _startedElements.size() + " events (should be one)."); 545 } 546 _startedElements.remove(_startedElements.size() - 1); 547 super.endDocument(); 548 } 549 } 550}