package restservices.publish; import static com.google.common.base.Preconditions.checkNotNull; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.ServletException; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import com.mendix.thirdparty.org.json.JSONObject; import restservices.RestServices; import restservices.proxies.HttpMethod; import restservices.publish.RestServiceHandler.HandlerRegistration; import restservices.publish.RestServiceRequest.RequestContentType; import restservices.publish.RestServiceRequest.ResponseType; import restservices.util.DataWriter; import restservices.util.ICloseable; import restservices.util.JSONSchemaBuilder; import restservices.util.JsonDeserializer; import restservices.util.JsonSerializer; import restservices.util.UriTemplate; import restservices.util.Utils; import system.proxies.FileDocument; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.mendix.core.Core; import com.mendix.core.CoreException; import com.mendix.systemwideinterfaces.core.IDataType; import com.mendix.systemwideinterfaces.core.IMendixObject; import com.mendix.systemwideinterfaces.core.meta.IMetaObject; import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive; public class MicroflowService implements IRestServiceHandler{ private String microflowname; private boolean hasArgument; private String argType; private boolean isReturnTypePrimitive; private String returnType; private String argName; private String roleOrMicroflow; private String description; private HttpMethod httpMethod; private boolean isFileSource = false; private boolean isFileTarget = false; private static final ServletFileUpload servletFileUpload = new ServletFileUpload(new DiskFileItemFactory(100000, new File(System.getProperty("java.io.tmpdir")))); private String relativeUrl; private HandlerRegistration serviceHandler; private ICloseable metaserviceHandler; private static final List<MicroflowService> microflowServices = Lists.newCopyOnWriteArrayList(); public MicroflowService(String microflowname, String roleOrMicroflow, HttpMethod httpMethod, String pathTemplateString, String description) throws CoreException { checkNotNull(microflowname); checkNotNull(roleOrMicroflow); checkNotNull(httpMethod); this.microflowname = microflowname; this.roleOrMicroflow = roleOrMicroflow; this.description = description; this.httpMethod = httpMethod; if (pathTemplateString != null) this.relativeUrl = Utils.removeLeadingAndTrailingSlash(pathTemplateString); else this.relativeUrl = microflowname.split("\\.")[1].toLowerCase(); this.consistencyCheck(); register(); } private void register() { unregister(); microflowServices.add(this); serviceHandler = RestServiceHandler.registerServiceHandler(httpMethod, getRelativeUrl(), roleOrMicroflow, this); metaserviceHandler = RestServiceHandler.registerServiceHandlerMetaUrl(getRelativeUrl()); } public void unregister() { microflowServices.remove(this); if (serviceHandler != null) { serviceHandler.close(); } if (metaserviceHandler != null) { metaserviceHandler.close(); } } private String getRelativeUrl() { return relativeUrl; } public MicroflowService(String microflowname, String securityRoleOrMicroflow, HttpMethod httpMethod, String description) throws CoreException { this(microflowname, securityRoleOrMicroflow, httpMethod, null, description); } private void consistencyCheck() throws CoreException { String secError = ConsistencyChecker.checkAccessRole(this.roleOrMicroflow); if (secError != null) throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ": " + secError); int argCount = Utils.getArgumentTypes(microflowname).size(); if (argCount > 1) throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", it should exist and have exactly zero or one argument"); hasArgument = argCount == 1; List<String> pathParams = new UriTemplate(relativeUrl).getTemplateVariables(); if (pathParams.size() > 0 && !hasArgument) { throw new IllegalArgumentException("Cannot publish microflow " + microflowname + " with path '" + relativeUrl + ", the microflow should have a single input argument object with at least attributes " + pathParams); } if (hasArgument) { IDataType argtype = Utils.getFirstArgumentType(microflowname); if (!argtype.isMendixObject()) throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", it should have a single object as input argument"); this.argType = argtype.getObjectType(); this.argName = Utils.getArgumentTypes(microflowname).keySet().iterator().next(); isFileSource = Core.isSubClassOf(FileDocument.entityName, argType); IMetaObject metaObject = Core.getMetaObject(argType); if (metaObject.isPersistable() && !isFileSource) throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", it should have a transient object of filedocument as input argument"); Set<String> metaPrimitiveNames = Sets.newHashSet(); for(IMetaPrimitive prim : metaObject.getMetaPrimitives()) { metaPrimitiveNames.add(prim.getName().toLowerCase()); } for(String pathParam : pathParams) { if (!metaPrimitiveNames.contains(pathParam.toLowerCase())) throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", its input argument should have an attribute with name '" + pathParam +"', as required by the template path"); } } if (httpMethod == null) { throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", it has no HTTP method defined."); } IDataType returnTypeFromMF = Core.getReturnType(microflowname); if (returnTypeFromMF.isMendixObject() || returnTypeFromMF.isList()){ this.returnType = returnTypeFromMF.getObjectType(); isFileTarget = Core.isSubClassOf(FileDocument.entityName, this.returnType); if (Core.getMetaObject(this.returnType).isPersistable() && !isFileTarget) throw new IllegalArgumentException("Cannot publish microflow " + microflowname+ ", its return type should be a non-persistable object or a file document"); } else isReturnTypePrimitive = true; } @Override public void execute(final RestServiceRequest rsr, Map<String, String> params) throws Exception { if (params.containsKey(RestServices.PARAM_ABOUT)) { serveDescription(rsr); } else { Map<String, Object> args = new HashMap<String, Object>(); IMendixObject inputObject = parseInputData(rsr, params); if(inputObject != null) args.put(argName, inputObject); if (isReturnTypePrimitive) rsr.setResponseContentType(ResponseType.PLAIN); //default, but might be overriden by the executing mf else if (isFileTarget) rsr.setResponseContentType(ResponseType.BINARY); Object result = Core.execute(rsr.getContext(), microflowname, args); writeOutputData(rsr, result); } } private void writeOutputData(RestServiceRequest rsr, Object result) throws IOException, Exception { if (result == null) { //write nothing } else if (this.isFileTarget) { if (!Utils.hasDataAccess(Core.getMetaObject(argType), rsr.getContext())) throw new IllegalStateException("Cannot serialize filedocument of type '" + argType + "', the object is not accessiable for users with role " + rsr.getContext().getSession().getUserRolesNames() + ". Please check the access rules"); String filename = ((IMendixObject)result).getValue(rsr.getContext(), FileDocument.MemberNames.Name.toString()); if (filename != null && !filename.isEmpty()) rsr.response.setHeader(RestServices.HEADER_CONTENTDISPOSITION, "attachment;filename=\"" + Utils.urlEncode(filename) + "\""); InputStream stream = Core.getFileDocumentContent(rsr.getContext(), (IMendixObject)result); IOUtils.copy(stream, rsr.response.getOutputStream()); } else if (this.isReturnTypePrimitive) { rsr.write(result == null ? "" : String.valueOf(result)); } else if (result instanceof List<?>) { rsr.startDoc(); rsr.datawriter.array(); for(Object item : (List<?>)result) rsr.datawriter.value(JsonSerializer.writeMendixObjectToJson(rsr.getContext(), (IMendixObject) item, true)); rsr.datawriter.endArray(); rsr.endDoc(); } else if (result instanceof IMendixObject) { rsr.startDoc(); rsr.datawriter.value(JsonSerializer.writeMendixObjectToJson(rsr.getContext(), (IMendixObject) result, true)); rsr.endDoc(); } else throw new IllegalStateException("Unexpected result from microflow " + microflowname + ": " + result.getClass().getName()); } private IMendixObject parseInputData(RestServiceRequest rsr, Map<String, String> params) throws IOException, ServletException, Exception { if (!hasArgument) return null; if (!Utils.hasDataAccess(Core.getMetaObject(argType), rsr.getContext())) throw new IllegalStateException("Cannot instantiate object of type '" + argType + "', the object is not accessiable for users with role " + rsr.getContext().getSession().getUserRolesNames() + ". Please check the access rules"); IMendixObject argObject = Core.instantiate(rsr.getContext(), argType); JSONObject data = new JSONObject(); //multipart data if (rsr.getRequestContentType() == RequestContentType.MULTIPART) { parseMultipartData(rsr, argObject, data); } //json data else if (rsr.getRequestContentType() == RequestContentType.JSON || (rsr.getRequestContentType() == RequestContentType.OTHER && !isFileSource)) { String body = IOUtils.toString(rsr.request.getInputStream()); data = new JSONObject(StringUtils.isEmpty(body) ? "{}" : body); } //not multipart but expecting a file? else if (isFileSource) { Core.storeFileDocumentContent(rsr.getContext(), argObject, rsr.request.getInputStream()); } RestServiceHandler.paramMapToJsonObject(params, data); //serialize to Mendix Object JsonDeserializer.readJsonDataIntoMendixObject(rsr.getContext(), data, argObject, false); return argObject; } private void parseMultipartData(RestServiceRequest rsr, IMendixObject argO, JSONObject data) throws IOException, FileUploadException { boolean hasFile = false; for(FileItemIterator iter = servletFileUpload.getItemIterator(rsr.request); iter.hasNext();) { FileItemStream item = iter.next(); if (!item.isFormField()){ //This is the file(?!) if (!isFileSource) { RestServices.LOGPUBLISH.warn("Received request with binary data but input argument isn't a filedocument. Skipping. At: " + rsr.request.getRequestURL().toString()); continue; } if (hasFile) RestServices.LOGPUBLISH.warn("Received request with multiple files. Only one is supported. At: " + rsr.request.getRequestURL().toString()); hasFile = true; Core.storeFileDocumentContent(rsr.getContext(), argO, determineFileName(item), item.openStream()); } else data.put(item.getFieldName(), IOUtils.toString(item.openStream())); } } private String determineFileName(FileItemStream item) { return null; //TODO: } public String getName() { return microflowname.split("\\.")[1].toLowerCase(); } public String getRequiredRoleOrMicroflow() { return roleOrMicroflow; } public void serveDescription(RestServiceRequest rsr) { rsr.startDoc(); if (rsr.getResponseContentType() == ResponseType.HTML) rsr.write("<h1>Operation: ").write(getRelativeUrl()).write("</h1>"); DataWriter objectWriter = rsr.datawriter.object() .key("name").value(getRelativeUrl()) .key("description").value(description) .key("url").value(RestServices.getAbsoluteUrl(getRelativeUrl())); if (getHttpMethod() == "GET") { objectWriter.key("arguments").value( hasArgument ? JSONSchemaBuilder.build(Utils.getFirstArgumentType(microflowname)) : null); } objectWriter.key("accepts_binary_data").value(isFileSource); IDataType returnType = Core.getReturnType(microflowname); if (!returnType.isNothing()) { objectWriter.key("result").value(isFileTarget ? RestServices.CONTENTTYPE_OCTET + " stream" : JSONSchemaBuilder.build(returnType)); } objectWriter.endObject(); rsr.endDoc(); } public String getHttpMethod() { return httpMethod == null ? null : httpMethod.toString(); } public static void clearMicroflowServices() { while (!microflowServices.isEmpty()) microflowServices.get(0).unregister(); } }