/**
* $Id: EntityBatchHandler.java 113499 2012-09-25 01:13:56Z azeckoski@unicon.net $
* $URL: https://source.sakaiproject.org/svn/entitybroker/trunk/rest/src/java/org/sakaiproject/entitybroker/rest/EntityBatchHandler.java $
* EntityBatchHandler.java - entity-broker - Dec 18, 2008 11:40:39 AM - azeckoski
**********************************************************************************
* Copyright (c) 2008, 2009 The 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.entitybroker.rest;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.azeckoski.reflectutils.ArrayUtils;
import org.azeckoski.reflectutils.map.ArrayOrderedMap;
import org.sakaiproject.entitybroker.EntityBrokerManager;
import org.sakaiproject.entitybroker.EntityView;
import org.sakaiproject.entitybroker.entityprovider.EntityProvider;
import org.sakaiproject.entitybroker.entityprovider.extension.Formats;
import org.sakaiproject.entitybroker.exception.EntityException;
import org.sakaiproject.entitybroker.providers.EntityRequestHandler;
import org.sakaiproject.entitybroker.providers.ExternalIntegrationProvider;
import org.sakaiproject.entitybroker.rest.caps.BatchProvider;
import org.sakaiproject.entitybroker.util.http.EntityHttpServletRequest;
import org.sakaiproject.entitybroker.util.http.EntityHttpServletResponse;
import org.sakaiproject.entitybroker.util.http.HttpClientWrapper;
import org.sakaiproject.entitybroker.util.http.HttpRESTUtils;
import org.sakaiproject.entitybroker.util.http.HttpResponse;
import org.sakaiproject.entitybroker.util.http.URLData;
import org.sakaiproject.entitybroker.util.http.HttpRESTUtils.Method;
import org.sakaiproject.entitybroker.util.request.RequestUtils;
/**
* This handles batch operations internally as much as possible,
* the idea is to provide for a standard way to reduce huge numbers of calls down to 1 call to the server
* which puts the data together into a single response
*
* @author Aaron Zeckoski (azeckoski @ gmail.com)
*/
public class EntityBatchHandler {
public static final String CONFIG_BATCH_ENABLE = "entitybroker.batch.enable";
public static final boolean CONFIG_BATCH_DEFAULT = false;
private static final String HEADER_BATCH_STATUS = "batchStatus";
private static final String HEADER_BATCH_ERRORS = "batchErrors";
private static final String HEADER_BATCH_MAPPING = "batchMapping";
private static final String HEADER_BATCH_URLS = "batchURLs";
private static final String HEADER_BATCH_REFS = "batchRefs";
private static final String HEADER_BATCH_KEYS = "batchKeys";
private static final String HEADER_BATCH_METHOD = "batchMethod";
private static final String UNREFERENCED_PARAMS = "NoRefs";
/**
* This is the name of the parameter which is used to pass along the reference URLs to be batch processed
*/
public static final String REFS_PARAM_NAME = "_refs";
private static final String UNIQUE_DATA_PREFIX = "X-XqReplaceQX-X-";
private static String INTERNAL_SERVER_ERROR_STATUS_STRING = HttpServletResponse.SC_INTERNAL_SERVER_ERROR+"";
/**
* Empty constructor, must use setters to set the needed services
*/
public EntityBatchHandler() { }
/**
* Full constructor, use this to correctly construct this class,
* note that after construction, the entityRequestHandler must be set also
*/
public EntityBatchHandler(EntityBrokerManager entityBrokerManager,
EntityEncodingManager entityEncodingManager,
ExternalIntegrationProvider externalIntegrationProvider) {
super();
this.entityBrokerManager = entityBrokerManager;
this.entityEncodingManager = entityEncodingManager;
this.externalIntegrationProvider = externalIntegrationProvider;
init();
}
private EntityProvider batchEP = null;
public void init() {
// register the batch EP handler
if (this.externalIntegrationProvider.getConfigurationSetting(CONFIG_BATCH_ENABLE, CONFIG_BATCH_DEFAULT)) {
batchEP = new BatchProvider() {
public String getEntityPrefix() {
return EntityRequestHandler.BATCH;
}
public String getBaseName() {
return getEntityPrefix();
}
public ClassLoader getResourceClassLoader() {
return EntityDescriptionManager.class.getClassLoader();
}
public String[] getHandledOutputFormats() {
return EntityEncodingManager.HANDLED_OUTPUT_FORMATS;
}
};
this.entityBrokerManager.getEntityProviderManager().registerEntityProvider(batchEP);
} else {
// batch provider is disabled so do not show the docs for it - this empty on purpose
}
}
public void destroy() {
System.out.println("INFO: EntityBatchHandler: destroy()");
if (batchEP != null) {
try {
this.entityBrokerManager.getEntityProviderManager().unregisterEntityProvider(batchEP);
} catch (RuntimeException e) {
System.out.println("WARN: EntityBatchHandler: Unable to unregister the batch provider: " + e);
}
}
}
private EntityBrokerManager entityBrokerManager;
public void setEntityBrokerManager(EntityBrokerManager entityBrokerManager) {
this.entityBrokerManager = entityBrokerManager;
}
private EntityEncodingManager entityEncodingManager;
public void setEntityEncodingManager(EntityEncodingManager entityEncodingManager) {
this.entityEncodingManager = entityEncodingManager;
}
private ExternalIntegrationProvider externalIntegrationProvider;
public void setExternalIntegrationProvider(ExternalIntegrationProvider externalIntegrationProvider) {
this.externalIntegrationProvider = externalIntegrationProvider;
}
/**
* Can only set this after the class is constructed since it forms a circular dependency,
* this is being set by the setter/constructor in the EntityHandlerImpl
*/
private EntityHandlerImpl entityRequestHandler;
public void setEntityRequestHandler(EntityHandlerImpl entityRequestHandler) {
this.entityRequestHandler = entityRequestHandler;
}
private String getServletContext() {
return this.entityBrokerManager.getServletContext();
}
private String getServletBatch() {
return getServletContext() + EntityRequestHandler.SLASH_BATCH;
}
/**
* Handle the batch operations encoded in this view and request
* @param view the current view
* @param req the current request
* @param res the current response
*/
public void handleBatch(EntityView view, HttpServletRequest req, HttpServletResponse res) {
if (view == null || req == null || res == null) {
throw new IllegalArgumentException("Could not process batch: invalid arguments, no args can be null (view="+view+",req="+req+",res="+res+")");
}
if (!externalIntegrationProvider.getConfigurationSetting(CONFIG_BATCH_ENABLE, CONFIG_BATCH_DEFAULT)) {
//log.info("Batch provider is disabled by default/property. See SAK-22619");
try {
res.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, "Batch provider is disabled by sakai config: "+CONFIG_BATCH_ENABLE+"=false. Enable this config setting with "+CONFIG_BATCH_ENABLE+"=true to enable batch handling. See SAK-22619 for details.");
} catch (IOException e) {
throw new RuntimeException("Cannot send error: res.sendError: "+e, e);
}
return;
}
// first find out which METHOD we are dealing with
String reqMethod = req.getMethod() == null ? EntityView.Method.GET.name() : req.getMethod().toUpperCase().trim();
Method method = HttpRESTUtils.makeMethodFromString(reqMethod);
if (Method.GET.equals(method)
|| Method.POST.equals(method)
|| Method.PUT.equals(method)
|| Method.DELETE.equals(method)) {
// valid methods
res.setHeader(HEADER_BATCH_METHOD, method.name());
} else {
throw new IllegalArgumentException("Cannot batch "+reqMethod+" request method, cannot continue processing request: " + view);
}
// now get to handling stuff
String format = view.getFormat();
String servletContext = getServletContext(); // will be the servlet context (e.g. /direct)
// validate the the refs param
String[] refs = getRefsOrFail(req);
// decode the params into a set of reference params
Map<String, Map<String, String[]>> referencedParams = extractReferenceParams(req, method, refs);
// loop through all references
HashSet<String> processedRefsAndURLs = new HashSet<String>(); // holds all refs which we processed in this batch
HashMap<String, String> dataMap = new ArrayOrderedMap<String, String>(); // the returned content data from each ref
Map<String, ResponseBase> results = new ArrayOrderedMap<String, ResponseBase>(); // the results of all valid refs
boolean successOverall = false; // true if all ok or partial ok, false if exception occurs or all fail
boolean failure = false;
for (int i = 0; i < refs.length; i++) {
String refKey = "ref" + i;
String reference = refs[i];
// validate the reference is not blank
if (reference == null || "".equals(reference)) {
continue; // skip
}
// skip refs that are already done, we do not process twice unless it is a POST
// NOTE: this duplicate check happens again slightly down below so change both at once
if (! Method.POST.equals(method)
&& processedRefsAndURLs.contains(reference)) {
System.out.println("WARN: EntityBatchHandler: Found a duplicate reference, this will not be processed: " + reference);
continue; // skip for GET/DELETE/PUT
}
// fix anything that does not start with a slash or http
String entityURL = reference;
if (! reference.startsWith("/")
&& ! reference.startsWith("http://")) {
// assume this is an EB entity url without the slash
entityURL = servletContext + EntityView.SEPARATOR + reference;
}
// make sure no one tries to batch a batch
if (reference.startsWith(EntityRequestHandler.SLASH_BATCH)
|| reference.startsWith( getServletBatch() )) {
throw new EntityException("Failure processing batch request, "
+ "batch reference ("+reference+") ("+entityURL+") appears to be another "
+ "batch URL (contains "+EntityRequestHandler.SLASH_BATCH+"), "
+ "failure in batch request: " + view,
EntityRequestHandler.SLASH_BATCH,
HttpServletResponse.SC_BAD_REQUEST);
}
// in case there are external ones we will reuse this httpclient
HttpClientWrapper clientWrapper = null;
// object will hold the results of this reference request
ResponseBase result = null;
// parse the entityURL, should hopefully not cause a failure
URLData ud = new URLData(entityURL);
/*
* identify the EB direct operations -
* this allows us to strip extensions and cleanup direct URLs as needed,
* possibly also handle these specially later on if desired,
* only EB operations can be handled internally
*/
if ( servletContext.equals(ud.contextPath) ) {
if (ud.pathInfo == null || "".equals(ud.pathInfo)) {
// looks like this servlet only with no path and we do not process that
continue;
}
boolean success = false;
try {
// parse the entityURL to verify it
entityBrokerManager.parseReference(ud.pathInfo);
success = true;
} catch (IllegalArgumentException e) {
String errorMessage = "Failure parsing direct entityURL ("+entityURL+") from reference ("+reference+") from path ("+ud.pathInfo+"): " + e.getMessage() + ":" + e.getCause();
System.out.println("WARN: EntityBatchHandler: " + errorMessage);
result = new ResponseError(reference, entityURL, errorMessage);
}
if (success) {
if (Method.GET.equals(method)) {
// rebuild the entityURL with the correct extension in there for GET
StringBuilder sb = new StringBuilder();
sb.append( servletContext );
sb.append(ud.pathInfoNoExtension);
sb.append(EntityView.PERIOD);
sb.append(format);
if (ud.query.length() > 0) {
// add on the query string
sb.append('?');
sb.append(ud.query);
}
entityURL = sb.toString();
}
// skip URLs that are already done, we do not process twice unless it is a POST
// NOTE: this duplicate check happens again slightly above so change both at once
if (! Method.POST.equals(method)
&& processedRefsAndURLs.contains(entityURL)) {
System.out.println("WARN: EntityBatchHandler: Found a duplicate entityURL, this will not be processed: " + entityURL);
continue; // skip
}
result = generateInternalResult(refKey, reference, entityURL, req, res, method, referencedParams);
}
} else {
// non-EB URL so we have to fire it off using the HttpUtils
// http utils requires full URLs
entityURL = makeFullExternalURL(req, entityURL);
// set the client wrapper with cookies so we can reuse it for efficiency
if (clientWrapper == null) {
clientWrapper = HttpRESTUtils.makeReusableHttpClient(false, 0, req.getCookies());
}
result = generateExternalResult(refKey, reference, entityURL, method, referencedParams, clientWrapper);
}
// special handling for null result (should really not happen unless there was a logic error)
if (result == null) {
successOverall = false;
failure = true;
throw new IllegalStateException("Somehow the result is null, this should never happen, fatal error");
}
if (result instanceof ResponseError) {
// looks like a failure occurred, keep going though
successOverall = false;
failure = true;
} else {
// all ok, process data
int status = result.getStatus();
if (status >= 200 && status < 300) {
successOverall = true;
}
if (status == HttpServletResponse.SC_NO_CONTENT) {
// no content to process
((ResponseResult)result).content = null;
((ResponseResult)result).data = null;
} else {
// process the content and see if it matches the expected result, if not we have to dump it in escaped
String content = ((ResponseResult)result).content;
String dataKey = checkContent(format, content, refKey, dataMap);
((ResponseResult)result).setDataKey(dataKey);
}
}
// store the processed ref and url so we do not do them again
processedRefsAndURLs.add(reference);
processedRefsAndURLs.add(entityURL);
results.put(refKey, result); // use an artificial key
}
// determine overall status
int overallStatus = HttpServletResponse.SC_OK;
if (failure == true || successOverall == false) {
overallStatus = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
// die if every ref was invalid
if (results.size() == 0) {
throw new EntityException("Invalid request which resulted in no valid references to batch process, original _refs=("
+ArrayUtils.arrayToString(refs)+")", EntityRequestHandler.SLASH_BATCH, HttpServletResponse.SC_BAD_REQUEST);
}
// compile all the responses into encoded data
String overallData = entityEncodingManager.encodeData(results, format, "refs", null);
if (Formats.XML.equals(format)) {
overallData = EntityEncodingManager.XML_HEADER + overallData;
}
// replace the data unique keys if there are any
overallData = reintegrateDataContent(format, dataMap, overallData);
// put response, headers, and code into the http response
applyOverallHeaders(res, results);
// put content into the response
try {
res.getWriter().write(overallData);
} catch (IOException e) {
throw new RuntimeException("Unable to encode data for overall response: " + e.getMessage(), e);
}
// set encoding
RequestUtils.setResponseEncoding(format, res);
// set overall status code
res.setStatus(overallStatus);
}
/**
* This will decode the set of params into a group of reference params based on the set of references
* @param req the current request
* @param method the current method
* @param refs the array of references
* @return the map of reference params with unreferenced params under the {@value #UNREFERENCED_PARAMS} key
* and everything else in ref# -> params map
*/
@SuppressWarnings("unchecked")
private Map<String, Map<String, String[]>> extractReferenceParams(
HttpServletRequest req, Method method, String[] refs) {
// Decode params into a reference map based on the refs for POST/PUT
ArrayOrderedMap<String, Map<String, String[]>> referencedParams = null;
if (Method.POST.equals(method) || Method.PUT.equals(method) ) {
referencedParams = new ArrayOrderedMap<String, Map<String, String[]>>();
Map<String, String[]> params = req.getParameterMap();
// create the maps to hold the params
referencedParams.put(UNREFERENCED_PARAMS, new ArrayOrderedMap<String, String[]>(params.size()));
for (int i = 0; i < refs.length; i++) {
String refKey = "ref" + i + '.';
referencedParams.put(refKey, new ArrayOrderedMap<String, String[]>(params.size()));
}
// put all request params into the map
for (Entry<String, String[]> entry : params.entrySet()) {
if (REFS_PARAM_NAME.equals(entry.getKey())) {
continue; // skip over the refs param
}
boolean found = false;
for (String refKey : referencedParams.keySet()) {
if (entry.getKey().startsWith(refKey)) {
String key = entry.getKey();
// fix key by removing the ref#. prefix
key = key.substring(refKey.length());
if (key.length() == 0) {
System.out.println("WARN: EntityBatchHandler: " +
"Skipping invalid reference param name ("+entry.getKey()+"), " +
"name must start with ref#. but MUST have the actual name of the param after that");
} else {
referencedParams.get(refKey).put(key, entry.getValue());
}
found = true;
break;
}
}
if (!found) {
// put the values into the unreferenced params portion of the map
referencedParams.get(UNREFERENCED_PARAMS).put(entry.getKey(), entry.getValue());
}
}
}
return referencedParams;
}
/**
* Apply the headers to the batched response,
* these headers are applied to all responses
* @param res the response to apply headers to
* @param results the results of the requests
*/
private void applyOverallHeaders(HttpServletResponse res, Map<String, ResponseBase> results) {
// set overall headers - batchRefs, batchKeys, batchStatus, batchErrors, batchInvalidRefs
int count = 0;
for (Entry<String, ResponseBase> entry : results.entrySet()) {
String refKey = entry.getKey();
ResponseBase refResp = entry.getValue();
if (count == 0) {
res.setHeader(HEADER_BATCH_KEYS, refKey);
res.setHeader(HEADER_BATCH_REFS, refResp.getReference());
res.setHeader(HEADER_BATCH_URLS, refResp.getEntityURL());
res.setHeader(HEADER_BATCH_MAPPING, refKey + "=" + refResp.getReference());
} else {
res.addHeader(HEADER_BATCH_KEYS, refKey);
res.addHeader(HEADER_BATCH_REFS, refResp.getReference());
res.addHeader(HEADER_BATCH_URLS, refResp.getEntityURL());
res.addHeader(HEADER_BATCH_MAPPING, refKey + "=" + refResp.getReference());
}
if (refResp.isFailure()) {
res.addHeader(HEADER_BATCH_ERRORS, refKey);
res.addHeader(HEADER_BATCH_STATUS, INTERNAL_SERVER_ERROR_STATUS_STRING);
} else {
int status = ((ResponseResult) refResp).getStatus();
res.addHeader(HEADER_BATCH_STATUS, Integer.toString(status));
}
count++;
}
}
/**
* Processing internal (EB) requests
* @return the result from the request (may be an error)
*/
private ResponseBase generateInternalResult(String refKey, String reference, String entityURL, HttpServletRequest req,
HttpServletResponse res, Method method, Map<String, Map<String, String[]>> referencedParams) {
ResponseBase result = null;
ResponseError error = null;
/* WARNING: This is important to understand why this was done as is
* First of all, forget the servlet forwarding, it is hopeless.
* Why you ask? This is why, tomcat 5 has issues with calling forward using a set of custom
* httpservelet* objects, it REQUIRES objects that are tomcat objects and attempts to use and
* even cast to those objects. This causes 2 failures:
* 1) tomcat attempts to access specific attributes from the request which are not there in
* other request objects, thus it skips over processing the request entirely... without marking it as failed...
* 2) tomcat attempts to cast the response object to a tomcat object after most of the processing
* is complete and causes a ClassCastException which blows up everything
*
* The alternatives are using httpclient for everything (maybe not a bad plan)
* or calling the entity request handler directly, this has its own issues in that
* it actually causes problems with redirection and requires injecting things
* which depend on each other
*
* One last note on this, this all works fine in Jetty... tomcat fail
* Fun times for all
*/
EntityHttpServletRequest entityRequest = new EntityHttpServletRequest(req, entityURL);
entityRequest.setContextPath("");
if (Method.POST.equals(method) || Method.PUT.equals(method) ) {
// set only the unreferenced and correct referenced params for this request
entityRequest.clearParameters(); // also clears REFS_PARAM_NAME
entityRequest.setParameters( referencedParams.get(UNREFERENCED_PARAMS) );
String key = refKey + '.';
if (referencedParams.containsKey(key)) {
entityRequest.setParameters( referencedParams.get(key) );
}
// set the params from the query itself again
entityRequest.setParameters( entityRequest.pathQueryParams );
//log.info("All request params: " + entityRequest.getStringParameters());
} else {
entityRequest.removeParameter(REFS_PARAM_NAME); // make sure this is not passed along
}
entityRequest.setUseRealDispatcher(false); // we do not want to actually have the container handle forwarding
EntityHttpServletResponse entityResponse = new EntityHttpServletResponse(res);
boolean redirected = false;
do {
try {
entityRequestHandler.handleEntityAccess(entityRequest, entityResponse, null);
redirected = false; // assume no redirect
} catch (Exception e) {
String errorMessage = "Failure attempting to process reference ("+reference+") for url ("+entityURL+"): " + e.getMessage() + ":" + e;
System.out.println("WARN: EntityBatchHandler: " + errorMessage);
error = new ResponseError(reference, entityURL, errorMessage);
break; // take us out if there is a failure
}
// Must handle all redirects manually despite the annoyance - this is really crappy but oh well
if (entityResponse.isRedirected()) {
String redirectURL = entityResponse.getRedirectedUrl();
if (redirectURL == null || redirectURL.length() == 0) {
throw new EntityException("Failed to find redirect URL when redirect was indicated by status ("+entityResponse.getStatus()+") for reference ("+reference+")", reference);
}
entityURL = redirectURL;
// check that the redirect is not external
if ( entityURL.startsWith(getServletContext()) ) {
// internal
entityRequest.setPathString(redirectURL);
entityResponse.reset();
redirected = true;
} else {
// TODO find a way to handle an external URL here
redirected = false;
}
}
} while (redirected);
/** OLD CODE which can't work in tomcat 5
// setup the request and response objects to do the reference request
RequestDispatcher dispatcher = req.getRequestDispatcher(entityURL); // should only be the relative path from this webapp
// the request needs to get the full url or path though
EntityHttpServletRequest entityRequest = new EntityHttpServletRequest(req, req.getContextPath() + entityURL);
entityRequest.setContextPath("");
entityRequest.removeParameter(REFS_PARAM_NAME); // make sure this is not passed along
EntityHttpServletResponse entityResponse = new EntityHttpServletResponse(res);
// fire off the URLs to the server and get back responses
try {
// need to forward instead of include to get headers back
dispatcher.forward(entityRequest, entityResponse);
} catch (Exception e) {
String errorMessage = "Failure attempting to process reference ("+reference+"): " + e.getMessage() + ":" + e;
log.warn(errorMessage, e);
error = new ResponseError(reference, entityURL, errorMessage);
}
**/
// create the result object to encode and place into the final response
if (error == null && entityResponse != null) {
// all ok, create the result for the response object
// all cookies go into the main response
Cookie[] cookies = entityResponse.getCookies();
for (Cookie cookie : cookies) {
res.addCookie(cookie);
}
// status codes are compiled
int status = entityResponse.getStatus();
// create the result (with raw content)
result = new ResponseResult(reference, entityURL, status, entityResponse.getHeaders(), entityResponse.getContentAsString());
} else {
result = error;
}
return result;
}
/**
* Processing external (non-EB) requests
* @return the result from the request (may be an error)
*/
private ResponseBase generateExternalResult(String refKey, String reference, String entityURL, Method method,
Map<String, Map<String, String[]>> referencedParams, HttpClientWrapper clientWrapper) {
ResponseBase result = null;
ResponseError error = null;
boolean guaranteeSSL = false;
// TODO allow enabling SSL?
Map<String, String> params = null;
if (referencedParams != null && ! referencedParams.isEmpty()) {
params = new ArrayOrderedMap<String, String>(referencedParams.size());
// put all unreferenced params in
Map<String, String[]> urp = referencedParams.get(UNREFERENCED_PARAMS);
for (Entry<String, String[]> entry : urp.entrySet()) {
String name = entry.getKey();
String value;
if (entry.getValue() == null || entry.getValue().length == 0) {
value = "";
} else {
value = entry.getValue()[0];
}
params.put(name, value);
}
if (Method.POST.equals(method) || Method.PUT.equals(method) ) {
// put all referenced params in for this key
String key = refKey + '.';
Map<String, String[]> rp = referencedParams.get(key);
if (rp != null) {
for (Entry<String, String[]> entry : rp.entrySet()) {
String name = entry.getKey();
String value;
if (entry.getValue() == null || entry.getValue().length == 0) {
value = "";
} else {
value = entry.getValue()[0];
}
params.put(name, value);
}
}
}
}
// fire off the request and hope it does not die horribly
HttpResponse httpResponse = null;
try {
httpResponse = HttpRESTUtils.fireRequest(clientWrapper,
entityURL,
method,
params, null, guaranteeSSL);
} catch (RuntimeException e) {
String errorMessage = "Failure attempting to process external URL ("+entityURL+") from reference ("+reference+"): " + e.getMessage() + ":" + e;
System.out.println("WARN: EntityBatchHandler: " + errorMessage);
error = new ResponseError(reference, entityURL, errorMessage);
}
// create the result object to encode and place into the final response
if (error == null && httpResponse != null) {
result = new ResponseResult(reference, entityURL, httpResponse.getResponseCode(),
httpResponse.getResponseHeaders(), httpResponse.getResponseBody());
} else {
result = error;
}
return result;
}
/**
* Creates a full URL so that the request can be sent
* @param req the request
* @param entityURL the partial URL (e.g. /thing/blah)
* @return a full URL (e.g. http://server/thing/blah)
*/
private String makeFullExternalURL(HttpServletRequest req, String entityURL) {
if (entityURL.startsWith("/")) {
// http client can only deal in complete URLs - e.g. "http://localhost:8080/thing"
String serverName = "localhost"; // req.getServerName();
try {
InetAddress i4 = Inet4Address.getLocalHost();
serverName = i4.getHostAddress();
} catch (UnknownHostException e) {
// could not get address, try the fallback
serverName = "localhost";
}
int serverPort = req.getLocalPort(); // getServerPort();
String protocol = req.getScheme();
if (protocol == null || "".equals(protocol)) {
protocol = "http";
}
StringBuilder sb = new StringBuilder(); // the server URL
sb.append(protocol);
sb.append("://");
sb.append(serverName);
if (serverPort > 0) {
sb.append(":");
sb.append(serverPort);
}
// look up the server URL using a service?
entityURL = sb.toString() + entityURL;
}
return entityURL;
}
/**
* Gets the refs (batch URLs to handle) from the request if possible
* @param req the request
* @return the array of references
* @throws IllegalArgumentException if the refs canot be found
*/
private String[] getRefsOrFail(HttpServletRequest req) {
String[] refs = req.getParameterValues(REFS_PARAM_NAME);
if (refs == null || refs.length == 0) {
throw new IllegalArgumentException(REFS_PARAM_NAME + " parameter must be set (e.g. /direct/batch.json?"+REFS_PARAM_NAME+"=/sites/popular,/sites/newest)");
}
if (refs.length == 1) {
// process separated list, assume comma separated
String separator = req.getParameter("separator");
if (separator == null || "".equals(separator)) {
separator = ",";
}
String presplit = refs[0];
refs = presplit.split(separator);
if (refs == null || refs.length == 0) {
throw new IllegalStateException("Failure attempting to process the _refs ("+presplit+") listing, could not get the final list of refs out by splitting using the separator ("+separator+")");
}
}
if (refs.length <= 0) {
throw new IllegalArgumentException(REFS_PARAM_NAME + " parameter must be set and there must be at least 1 reference (e.g. /direct/batch.json?"+REFS_PARAM_NAME+"=/sites/popular,/sites/newest)");
}
return refs;
}
/**
* Takes the overall data and reintegrates any content data that is waiting to be merged,
* this may do nothing if there is no content to merge
* @return the integrated data content
*/
private String reintegrateDataContent(String format, HashMap<String, String> dataMap,
String overallData) {
StringBuilder sb = new StringBuilder();
int curLoc = 0;
for (Entry<String, String> entry : dataMap.entrySet()) {
// looping order is critical here, it must be in the same order it was added
if (entry.getKey() != null && ! "".equals(entry.getKey())) {
String key = entry.getKey();
String value = entry.getValue();
if (Formats.XML.equals(format)) {
value = "\n" + value; // add in a break
} else if (Formats.JSON.equals(format)) {
key = '"' + key + '"'; // have to also replace the quotes
}
int keyLoc = overallData.indexOf(key);
if (keyLoc > -1) {
sb.append( overallData.subSequence(curLoc, keyLoc) );
sb.append( value );
curLoc = keyLoc + key.length();
}
//overallData = overallData.replace(key, value);
}
}
// add in the remainder of the overall data string
sb.append( overallData.subSequence(curLoc, overallData.length()) );
return sb.toString();
}
/**
* Checks that the content is in the correct format and is not too large,
* if it is too large it will not be processed and if it is in the wrong format it will be encoded as a data chunk,
* it is OK it will be placed into the dataMap and reintegrated after encoding
* @param content this it the content of the response body (if null then no processing occurs, null returned)
* @return the dataKey which maps to the real content, need replace the key later OR null if the content is empty or too large
*/
private String checkContent(String format, String content, String refKey,
HashMap<String, String> dataMap) {
String dataKey = null;
if (content != null) {
content = content.trim();
if (! "".equals(content)) {
if (entityEncodingManager.validateFormat(content, format)) {
if (Formats.XML.equals(format)
|| Formats.HTML.equals(format)) {
// strip off the xml header and doctype if it exists
content = stripOutXMLTag(content, "<?", "?>");
content = stripOutXMLTag(content, "<!DOCTYPE", ">");
}
// valid for the current format so insert later instead of merging now
dataKey = UNIQUE_DATA_PREFIX + refKey;
dataMap.put(dataKey, content);
}
}
}
return dataKey;
}
/**
* This will strip any tag out of an xml file by finding the startTag (if possible),
* and then the endTag and chopping this out of the given content and returning
* the new value
* @param content any XML like content
* @param startTag the starting tag (e.g. "<?" or "<blah")
* @param endTag the ending tag (e.g "?>" or "/blah")
* @return the content without the chopped out part if found
*/
public String stripOutXMLTag(String content, String startTag, String endTag) {
if (startTag != null
&& ! "".equals(startTag)
&& endTag != null
&& ! "".equals(endTag)) {
int pos = content.indexOf(startTag);
if (pos >= 0) {
int end = content.indexOf(endTag, pos);
if (end > 0) {
StringBuilder sb = new StringBuilder();
if (pos > 0) {
sb.append( content.substring(0, pos) );
}
sb.append( content.substring(end + endTag.length()) );
content = sb.toString().trim();
}
}
}
return content;
}
/**
* Base class for all response data which will be encoded and output
*/
public static class ResponseBase {
public String reference;
public String getReference() {
return reference;
}
public String entityURL;
public String getEntityURL() {
return entityURL;
}
public int status;
public int getStatus() {
return status;
}
public transient boolean failure = false;
public boolean isFailure() {
return failure;
}
}
/**
* Holds the error values which will be encoded by the various EB utils
*/
public static class ResponseError extends ResponseBase {
public String error;
public ResponseError(String reference, String entityURL, String errorMessage) {
this.reference = reference;
this.entityURL = entityURL;
this.error = errorMessage;
this.failure = true;
this.status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
}
/**
* Holds the results from a successful response request
*/
public static class ResponseResult extends ResponseBase {
public Map<String, String[]> headers;
public String data;
/**
* Set the data key (clears the raw content) if the key is non-null
* @param data processed data
*/
public void setDataKey(String dataKey) {
if (dataKey != null) {
this.data = dataKey;
this.content = null;
}
}
/**
* The raw content from the request
*/
public String content;
public ResponseResult(String reference, String entityURL, int status, Map<String, String[]> headers) {
this.reference = reference;
this.entityURL = entityURL;
this.status = status;
this.headers = headers;
this.failure = false;
this.content = null;
this.data = null;
}
public ResponseResult(String reference, String entityURL, int status, Map<String, String[]> headers, String content) {
this.reference = reference;
this.entityURL = entityURL;
this.status = status;
this.headers = headers;
this.content = content;
this.data = null;
this.failure = false;
}
}
}