package restservices.publish;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newCopyOnWriteArrayList;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.exception.ExceptionUtils;
import com.mendix.thirdparty.org.json.JSONException;
import com.mendix.thirdparty.org.json.JSONObject;
import restservices.RestServices;
import restservices.consume.RestConsumeException;
import restservices.consume.RestConsumer;
import restservices.proxies.DataServiceDefinition;
import restservices.proxies.HttpMethod;
import restservices.proxies.RestServiceError;
import restservices.publish.RestPublishException.RestExceptionType;
import restservices.util.Function;
import restservices.util.ICloseable;
import restservices.util.UriTemplate;
import restservices.util.Utils;
import com.google.common.collect.Maps;
import com.mendix.core.Core;
import com.mendix.core.CoreException;
import com.mendix.externalinterface.connector.RequestHandler;
import com.mendix.integration.WebserviceException;
import com.mendix.m2ee.api.IMxRuntimeRequest;
import com.mendix.m2ee.api.IMxRuntimeResponse;
import com.mendix.systemwideinterfaces.core.IContext;
import com.mendix.systemwideinterfaces.core.ISession;
import communitycommons.XPath;
public class RestServiceHandler extends RequestHandler{
private static RestServiceHandler instance = null;
private static boolean started = false;
static class HandlerRegistration implements ICloseable {
final String method;
final UriTemplate template;
final String roleOrMicroflow;
final IRestServiceHandler handler;
HandlerRegistration(String method, UriTemplate template, String roleOrMicroflow, IRestServiceHandler handler) {
this.method = method;
this.template = template;
this.roleOrMicroflow = roleOrMicroflow;
this.handler = handler;
}
public boolean accepts(String method, RestServiceRequest rsr) {
return this.method.equals(method) ||
(HttpMethod.GET.toString().equals(method) &&
rsr.getRequestParameter(RestServices.PARAM_ABOUT, null) == "");
}
@Override
public String toString() {
return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
@Override
public void close() {
services.remove(this);
}
}
private static List<HandlerRegistration> services = newCopyOnWriteArrayList();
private static List<String> metaServiceUrls = newCopyOnWriteArrayList();
static {
registerServiceOverviewHandler();
}
public synchronized static void start(IContext context) throws Exception {
if (instance == null) {
RestServices.LOGPUBLISH.info("Starting RestServices module...");
instance = new RestServiceHandler();
boolean isSandbox = Core.getConfiguration().getApplicationRootUrl().contains(".mendixcloud.com") && Core.getConfiguration().isInDevelopment();
if (isSandbox)
startSandboxCompatibilityMode();
else
Core.addRequestHandler(RestServices.PATH_REST, instance);
started = true;
loadConfig(context);
RestServices.LOGPUBLISH.info("Starting RestServices module... DONE");
}
}
/**
* startSandboxCompatibilityMode is introduced to circumvent the fact that custom request handlers
* are not available in sandbox apps. In that case the 'ws-doc' requesthandler is reclaimed by the
* Rest services module as soon the app is started. Obviously this is a nasty workaround and this
* should only be used for demo / testing purposes but never be exposed in real live apps.
*/
private static void startSandboxCompatibilityMode() {
RestServices.PATH_REST = "ws-doc/";
final RestServiceHandler self = instance;
new Thread() {
@Override
public void run() {
boolean started = false;
while(!started) {
try {
Thread.sleep(1000);
RestConsumer.getObject(Core.createSystemContext(), Core.getConfiguration().getApplicationRootUrl() + "/ws/", null);
started = true;
} catch (RestConsumeException e) {
started = e.getResponseData().getStatus() != HttpStatus.SC_BAD_GATEWAY;
}
catch (Exception e) {
RestServices.LOGPUBLISH.warn("Error when trying to start sandbox mode: " + e.getMessage(), e);
break;
}
}
Core.addRequestHandler(RestServices.PATH_REST, self);
RestServices.LOGPUBLISH.warn("The RestServices module has been started on basepath 'ws-doc/' for sandbox compatibility. Please use the alternative basepath for demo & testing purposes only, and do not share this path as integration endpoint");
}
}.start();
}
private static void registerServiceOverviewHandler() {
registerServiceHandler(HttpMethod.GET, "/", "*", new IRestServiceHandler() {
@Override
public void execute(RestServiceRequest rsr,
Map<String, String> params) throws Exception {
ServiceDescriber.serveServiceOverview(rsr);
}
});
}
private static void loadConfig(IContext context) throws CoreException {
for (DataServiceDefinition def : XPath.create(context, DataServiceDefinition.class).all()) {
loadConfig(def, false, context);
}
}
public static void loadConfig(DataServiceDefinition def, boolean throwOnFailure, IContext context) {
if (!started)
return;
RestServices.LOGPUBLISH.info("Loading service " + def.getName()+ "...");
String errors = null;
try {
ConsistencyChecker.check(def);
}
catch(Exception e) {
errors = "Failed to run consistency checks: " + e.getMessage();
}
if (errors != null) {
String msg = "Failed to load service '" + def.getName() + "': \n" + errors;
RestServices.LOGPUBLISH.error(msg);
if (throwOnFailure)
throw new IllegalStateException(msg);
}
else {
DataService service = DataService.getServiceByDefinition(def);
if (service != null) {
service.unregister();
}
RestServices.LOGPUBLISH.info("Reloading definition of service '" + def.getName() + "'");
service = new DataService(def, context);
service.register();
RestServices.LOGPUBLISH.info("Loading service " + def.getName()+ "... DONE");
}
}
public static HandlerRegistration registerServiceHandler(HttpMethod method, String templatePath, String roleOrMicroflow, IRestServiceHandler handler) {
checkNotNull(method, "method");
HandlerRegistration handlerRegistration = new HandlerRegistration(method.toString(), new UriTemplate(templatePath), roleOrMicroflow, handler);
services.add(handlerRegistration);
RestServices.LOGPUBLISH.info("Registered data service on '" + method + " " + templatePath + "'");
return handlerRegistration;
}
private static void requestParamsToJsonMap(RestServiceRequest rsr, Map<String, String> params) {
for (String param : rsr.request.getParameterMap().keySet())
params.put(param, rsr.request.getParameter(param));
}
public static void paramMapToJsonObject(Map<String, String> params, JSONObject data) {
for(Entry<String, String> pathValue : params.entrySet())
data.put(pathValue.getKey(), pathValue.getValue());
}
private static void executeHandler(final RestServiceRequest rsr, String method, String relpath, ISession existingSession) throws Exception {
boolean pathExists = false;
final Map<String, String> params = Maps.newHashMap();
for (final HandlerRegistration reg : services) {
if (reg.template.match(relpath, params)) {
if (reg.accepts(method, rsr)) {
// Mixin query parameters
requestParamsToJsonMap(rsr, params);
// Execute the reqeust
if (rsr.authenticate(reg.roleOrMicroflow, existingSession)) {
rsr.withTransaction(new Function<Boolean>() {
@Override
public Boolean apply() throws Exception {
reg.handler.execute(rsr, params);
return true;
}
});
return;
} else {
throw new RestPublishException(RestExceptionType.UNAUTHORIZED, "Unauthorized. Please provide valid credentials or set up a Mendix user session");
}
} else {
pathExists = true;
}
}
}
if (pathExists) {
throw new RestPublishException(RestExceptionType.METHOD_NOT_ALLOWED, "Method not allowed for service at: '" + relpath + "'");
} else {
throw new RestPublishException(RestExceptionType.NOT_FOUND, "Unknown service at: '" + relpath + "'");
}
}
@Override
public void processRequest(IMxRuntimeRequest req, IMxRuntimeResponse resp,
String var3) {
long start = System.currentTimeMillis();
HttpServletRequest request = req.getHttpServletRequest();
HttpServletResponse response = resp.getHttpServletResponse();
String method = request.getMethod();
URL u;
try {
u = new URL(request.getRequestURL().toString());
} catch (MalformedURLException e1) {
throw new IllegalStateException(e1);
}
String relpath = u.getPath().substring(RestServices.PATH_REST.length() + 1);
String requestStr = method + " " + relpath;
response.setCharacterEncoding(RestServices.UTF8);
response.setHeader("Expires", "-1");
if (RestServices.LOGPUBLISH.isDebugEnabled())
RestServices.LOGPUBLISH.debug("incoming request: " + Utils.getRequestUrl(request));
RestServiceRequest rsr = new RestServiceRequest(request, response, resp, relpath);
try {
ISession existingSession = getSessionFromRequest(req);
executeHandler(rsr, method, relpath, existingSession);
if (RestServices.LOGPUBLISH.isDebugEnabled())
RestServices.LOGPUBLISH.debug("Served " + requestStr + " in " + (System.currentTimeMillis() - start) + "ms.");
}
catch(RestPublishException rre) {
handleRestPublishException(requestStr, rsr, rre);
}
catch(JSONException je) {
handleJsonException(requestStr, rsr, je);
}
catch(Throwable e) {
Throwable cause = ExceptionUtils.getRootCause(e);
if (cause instanceof RestPublishException)
handleRestPublishException(requestStr, rsr, (RestPublishException) cause);
else if (cause instanceof JSONException)
handleJsonException(requestStr, rsr, (JSONException) cause);
if (cause instanceof CustomRestServiceException) {
CustomRestServiceException rse = (CustomRestServiceException) cause;
RestServices.LOGPUBLISH.warn(String.format("Failed to serve %s: %d (code: %s): %s", requestStr, rse.getHttpStatus(), rse.getDetail(), rse.getMessage()));
serveErrorPage(rsr, rse.getHttpStatus(), rse.getMessage(), rse.getDetail());
}
else if (cause instanceof WebserviceException) {
RestServices.LOGPUBLISH.warn("Invalid request " + requestStr + ": " +cause.getMessage());
serveErrorPage(rsr, HttpStatus.SC_BAD_REQUEST, cause.getMessage(), ((WebserviceException) cause).getFaultCode());
}
else {
RestServices.LOGPUBLISH.error("Failed to serve " + requestStr + ": " +e.getMessage(), e);
serveErrorPage(rsr, HttpStatus.SC_INTERNAL_SERVER_ERROR, "Failed to serve: " + requestStr + ": An internal server error occurred. Please check the application logs or contact a system administrator.", null);
}
}
finally {
rsr.dispose();
}
}
public void handleJsonException(String requestStr, RestServiceRequest rsr,
JSONException je) {
RestServices.LOGPUBLISH.warn("Failed to serve " + requestStr + ": Invalid JSON: " + je.getMessage());
serveErrorPage(rsr, HttpStatus.SC_BAD_REQUEST, "JSON is incorrect. Please review the request data: " + je.getMessage(), "INVALID_JSON");
}
public void handleRestPublishException(String requestStr,
RestServiceRequest rsr, RestPublishException rre) {
RestServices.LOGPUBLISH.warn("Failed to serve " + requestStr + ": " + rre.getType() + " " + rre.getMessage());
serveErrorPage(rsr, rre.getStatusCode(), rre.getType().toString() + ": " + requestStr + " " + rre.getMessage(), rre.getType().toString());
}
private void serveErrorPage(RestServiceRequest rsr, int status, String error, String errorCode) {
rsr.response.reset();
rsr.response.setStatus(status);
//reques authentication
if (status == HttpStatus.SC_UNAUTHORIZED)
rsr.response.addHeader(RestServices.HEADER_WWWAUTHENTICATE, "Basic realm=\"Rest Services\"");
rsr.startDoc();
switch(rsr.getResponseContentType()) {
default:
case HTML:
rsr.write("<h1>" + error + "</h1>");
if (errorCode != null)
rsr.write("<p>Error code: " + errorCode + "</p>");
rsr.write("<p>Http status code: " + status + "</p>");
break;
case JSON:
case XML:
JSONObject data = new JSONObject();
data.put(RestServiceError.MemberNames.errorMessage.toString(), error);
if (errorCode != null && !errorCode.isEmpty())
data.put(RestServiceError.MemberNames.errorCode.toString(), errorCode);
rsr.datawriter.value(data);
break;
}
rsr.endDoc();
}
public static boolean isStarted() {
return started;
}
public static void clearServices() {
services.clear();
registerServiceOverviewHandler();
}
public static ICloseable registerServiceHandlerMetaUrl(final String serviceBaseUrl) {
checkArgument(isNotEmpty(serviceBaseUrl));
metaServiceUrls.add(serviceBaseUrl);
return new ICloseable() {
@Override
public void close() {
metaServiceUrls.remove(serviceBaseUrl);
}
};
}
public static List<String> getServiceBaseUrls() {
return metaServiceUrls;
}
}