mendix / RestServices

REST service module for Mendix. Supports consuming and publishing REST based services and real-time data synchronization. Supports JSON, form-encoded, multipart and binary data transport.
Apache License 2.0
31 stars 46 forks source link

Add support for named file parts and multiple file parts when making POST/PUT requests using multipart/formdata #40

Closed mweststrate closed 9 years ago

mweststrate commented 9 years ago

Suggested patch:

Index: RestConsumer.java
===================================================================
--- RestConsumer.java   (revision 5030)
+++ RestConsumer.java   (working copy)
@@ -6,10 +6,12 @@
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.TreeMap;

 import org.apache.commons.httpclient.Credentials;
@@ -57,11 +59,16 @@

 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
+import com.google.common.collect.Lists;
 import com.mendix.core.Core;
 import com.mendix.core.CoreException;
 import com.mendix.m2ee.api.IMxRuntimeResponse;
 import com.mendix.systemwideinterfaces.core.IContext;
+import com.mendix.systemwideinterfaces.core.IMendixIdentifier;
 import com.mendix.systemwideinterfaces.core.IMendixObject;
+import com.mendix.systemwideinterfaces.core.meta.IMetaAssociation;
+import com.mendix.systemwideinterfaces.core.meta.IMetaAssociation.AssociationType;
+import com.mendix.systemwideinterfaces.core.meta.IMetaObject;
 import communitycommons.StringUtils;

 public class RestConsumer {
@@ -415,9 +422,10 @@
            method = HttpMethod.GET;

        final boolean isFileSource = source != null && Core.isSubClassOf(FileDocument.entityName, source.getType()); 
-       final boolean isFileTarget = target != null && Core.isSubClassOf(FileDocument.entityName, target.getType()); 
+       final boolean isFileTarget = target != null && Core.isSubClassOf(FileDocument.entityName, target.getType());
+       final boolean hasFileParts = source != null && hasFileParts(source.getMetaObject());

-       if (isFileSource && !(method == HttpMethod.POST || method == HttpMethod.PUT))
+       if ((isFileSource || hasFileParts) && !(method == HttpMethod.POST || method == HttpMethod.PUT))
            throw new IllegalArgumentException("Files can only be send with method is POST or PUT");

        Map<String, String> requestHeaders = new HashMap<String, String>();
@@ -429,11 +437,13 @@
        boolean appendDataToUrl = source != null && (asFormData || method == HttpMethod.GET || method == HttpMethod.DELETE);
        url = updateUrlPathComponentsWithParams(url, appendDataToUrl, isFileSource, data, params);

+       RestServices.LOGCONSUME.info("isFile source "  + isFileSource + ", hasFileParts " + hasFileParts);
+       
        //Setup request entity for file
        if (!asFormData && isFileSource) {
            requestEntity = new InputStreamRequestEntity(Core.getFileDocumentContent(context, source));
        }
-       else if (source != null && asFormData && isFileSource) {
+       else if (source != null && asFormData && (isFileSource || hasFileParts)) {
            requestEntity = buildMultiPartEntity(context, source, params);
        }
        else if (asFormData && !isFileSource)
@@ -524,23 +534,57 @@

    private static RequestEntity buildMultiPartEntity(final IContext context,
            final IMendixObject source, Map<String, String> params)
-           throws IOException {
+           throws IOException, CoreException {
        //MWE: don't set contenttype to CONTENTTYPE_MULTIPART; this will be done by the request entity and add the boundaries
-       Part[] parts = new Part[params.size() + 1];
+       List<Part> parts = Lists.newArrayList();

-       String fileName = (String) source.getValue(context, FileDocument.MemberNames.Name.toString()); 
-       ByteArrayPartSource p = new ByteArrayPartSource(fileName, IOUtils.toByteArray(Core.getFileDocumentContent(context, source)));
-       parts[0] = new FilePart(fileName, p);
+       //This object self could be a filedocument
+       if (Core.isSubClassOf(FileDocument.entityName, source.getType())) {
+           addFilePart(context, getFileDocumentFileName(context, source), source, parts);
+       }
+       //.. or one of its children could be a filedocument. This way multiple file parts, or specifically named file parts can be send
+       for(String name : getAssociationsReferingToFileDocs(source.getMetaObject())) {
+           IMendixIdentifier subObject = (IMendixIdentifier) source.getValue(context, name);
+           params.remove(Utils.getShortMemberName(name));
+           if (subObject != null) {
+               addFilePart(context, Utils.getShortMemberName(name), Core.retrieveId(context, subObject), parts);
+           }
+       }
+
+       //serialize any other members as 'normal' key value pairs
+       for(Entry<String, String> e : params.entrySet()) {
+           parts.add(new StringPart(e.getKey(), e.getValue(), RestServices.UTF8));
+       }

-       int i = 1;
-       for(Entry<String, String> e : params.entrySet())
-           parts[i++] = new StringPart(e.getKey(), e.getValue(), RestServices.UTF8);
-       
        params.clear();
-       
-       return new MultipartRequestEntity(parts, new HttpMethodParams());
+       return new MultipartRequestEntity(parts.toArray(new Part[0]), new HttpMethodParams());
    }

+   private static Set<String> getAssociationsReferingToFileDocs(IMetaObject meta) {
+       Set<String> names = new HashSet<String>();
+       for (IMetaAssociation assoc : meta.getMetaAssociationsParent()) {
+           if (assoc.getType() == AssociationType.REFERENCE && Core.isSubClassOf(FileDocument.entityName, assoc.getChild().getName()))
+               names.add(assoc.getName());
+       }
+       return names;
+   }
+   
+   private static boolean hasFileParts(IMetaObject metaObject) {
+       return getAssociationsReferingToFileDocs(metaObject).size() > 0;
+   }
+
+   private static void addFilePart(final IContext context, String partname, final IMendixObject source, List<Part> parts) throws IOException {
+       ByteArrayPartSource p = new ByteArrayPartSource(
+               getFileDocumentFileName(context, source), 
+               IOUtils.toByteArray(Core.getFileDocumentContent(context, source)));
+       parts.add(new FilePart(partname, p));
+   }
+
+   private static String getFileDocumentFileName(final IContext context,
+           final IMendixObject source) {
+       return (String) source.getValue(context, FileDocument.MemberNames.Name.toString());
+   }
+   
    private static boolean isFileDocAttr(String key) {
        try {
            FileDocument.MemberNames.valueOf(key); 
mweststrate commented 9 years ago

Note that referring to filedocs from transient objects will generate a warning (no service defined) when serializing. Serialization should be updated with the purpose of serialization, if it is for consuming services, using other service definitions is never a useful option in the first place.