So, I've searched the whole web for this, but couldn't find why this one doesn't work. I've got pretty far until now I think, I'm just stuck at this currently very weird problem.
I've built a CXF webservice which allows people to upload photos (to be deployed in Google Apps Engine). It should be working fine, but: I don't know how to test this as it's multipart/related (so is the contract). No way to test this via PostMan, SOAPUI, or any other UI tester, so I went to testing from Java directly.
I've started from this article: http://www.jguru.com/faq/view.jsp?EID=735674, and went through these for examples of how my syntax should look like:
Heck, I even went through the source code of Apache CXF to find the source of the crash here GC - AttachmentDeserializer.
This is what I want to send:
--the_photo
Content-Type: application/json
{
"objectToLinkTo": "ad9821e6-d308-472b-b21f-308b08c3fa70"
}
--the_photo
Content-Type: image/jpeg
JPEG data
--the_photo--
This is what I do to try to send this:
import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Paths;
public class PhotoTest {
private static final String ADD_PHOTO = "http://localhost:8080/v1/photos";
private static void writeJson(String value, DataOutputStream out, String boundary) {
try {
out.writeBytes("Content-Type: application/json");
out.writeBytes("\n{'objectToLinkTo': '" + value + "'}");
out.writeBytes("\n--" + boundary);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void writeImage(String contentType, String file, DataOutputStream out, String boundary) {
try {
out.writeBytes("Content-Type: " + contentType + "\n");
java.nio.file.Path path = Paths.get(file);
byte[] data = Files.readAllBytes(path);
out.write(data);
out.writeBytes("\n--" + boundary + "--");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main (String args[]) {
try {
URL servlet = new URL(ADD_PHOTO);
URLConnection urlConnection = servlet.openConnection();
urlConnection.setDoOutput(true);
urlConnection.setDoInput(true);
urlConnection.setUseCaches(false);
String boundary = "the_photo";
urlConnection.setRequestProperty("Content-type", "multipart/related; boundary=" + boundary);
urlConnection.setRequestProperty("Cache-Control", "no-cache");
DataOutputStream out = new DataOutputStream(urlConnection.getOutputStream());
out.writeBytes("--" + boundary + "\n");
writeJson("ad9821e6-d308-472b-b21f-308b08c3fa70", out, boundary);
writeImage("image/jpeg", "K:\\Downloads\\test.jpg", out, boundary);
out.flush();
out.close();
InputStream stream = urlConnection.getInputStream();
BufferedInputStream in = new BufferedInputStream(stream);
int i = 0;
while ((i = in.read()) != -1) {
System.out.write(i);
}
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
The result at the server side (the code is locally deployed using mvn appengine:test
):
[INFO] 07:27:06,804 ERROR [commons.exceptions.exceptionwrapper.RuntimeExceptionWrapper] - Couldn't find MIME boundary: --the_photo
[INFO] org.apache.cxf.interceptor.Fault: Couldn't find MIME boundary: --the_photo
[INFO] at org.apache.cxf.interceptor.AttachmentInInterceptor.handleMessage(AttachmentInInterceptor.java:60)
[INFO] at org.apache.cxf.jaxrs.ext.MessageContextImpl.createAttachments(MessageContextImpl.java:267)
[INFO] at org.apache.cxf.jaxrs.ext.MessageContextImpl.get(MessageContextImpl.java:76)
[INFO] at org.apache.cxf.jaxrs.impl.tl.ThreadLocalMessageContext.get(ThreadLocalMessageContext.java:38)
[INFO] at org.apache.cxf.jaxrs.utils.multipart.AttachmentUtils.getMultipartBody(AttachmentUtils.java:114)
[INFO] at org.apache.cxf.jaxrs.utils.multipart.AttachmentUtils.getAttachments(AttachmentUtils.java:119)
[INFO] at org.apache.cxf.jaxrs.provider.MultipartProvider.readFrom(MultipartProvider.java:151)
[INFO] at org.apache.cxf.jaxrs.utils.JAXRSUtils.readFromMessageBodyReader(JAXRSUtils.java:1325)
[INFO] at org.apache.cxf.jaxrs.utils.JAXRSUtils.readFromMessageBody(JAXRSUtils.java:1276)
[INFO] at org.apache.cxf.jaxrs.utils.JAXRSUtils.processParameter(JAXRSUtils.java:815)
[INFO] at org.apache.cxf.jaxrs.utils.JAXRSUtils.processParameters(JAXRSUtils.java:778)
[INFO] at org.apache.cxf.jaxrs.interceptor.JAXRSInInterceptor.processRequest(JAXRSInInterceptor.java:212)
[INFO] at org.apache.cxf.jaxrs.interceptor.JAXRSInInterceptor.handleMessage(JAXRSInInterceptor.java:77)
[INFO] at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:307)
[INFO] at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121)
[INFO] at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:243)
[INFO] at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:223)
[INFO] at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:197)
[INFO] at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:149)
[INFO] at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:171)
[INFO] at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:286)
[INFO] at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doPost(AbstractHTTPServlet.java:206)
[INFO] at javax.servlet.http.HttpServlet.service(HttpServlet.java:637)
[INFO] at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:262)
[INFO] at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
[INFO] at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166)
[INFO] at com.google.appengine.api.socket.dev.DevSocketFilter.doFilter(DevSocketFilter.java:74)
[INFO] at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
[INFO] at com.google.appengine.tools.development.ResponseRewriterFilter.doFilter(ResponseRewriterFilter.java:127)
[INFO] at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
[INFO] at com.google.appengine.tools.development.HeaderVerificationFilter.doFilter(HeaderVerificationFilter.java:34)
[INFO] at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
[INFO] at com.google.appengine.api.blobstore.dev.ServeBlobFilter.doFilter(ServeBlobFilter.java:63)
[INFO] at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
[INFO] at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
[INFO] at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
[INFO] at com.google.appengine.tools.development.StaticFileFilter.doFilter(StaticFileFilter.java:125)
[INFO] at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
[INFO] at com.google.appengine.tools.development.DevAppServerModulesFilter.doDirectRequest(DevAppServerModulesFilter.java:366)
[INFO] at com.google.appengine.tools.development.DevAppServerModulesFilter.doDirectModuleRequest(DevAppServerModulesFilter.java:349)
[INFO] at com.google.appengine.tools.development.DevAppServerModulesFilter.doFilter(DevAppServerModulesFilter.java:116)
[INFO] at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
[INFO] at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
[INFO] at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
[INFO] at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
[INFO] at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
[INFO] at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
[INFO] at com.google.appengine.tools.development.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:98)
[INFO] at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
[INFO] at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:491)
[INFO] at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
[INFO] at org.mortbay.jetty.Server.handle(Server.java:326)
[INFO] at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
[INFO] at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:938)
[INFO] at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:755)
[INFO] at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:218)
[INFO] at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
[INFO] at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:409)
[INFO] at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)
[INFO] Caused by: java.io.IOException: Couldn't find MIME boundary: --the_photo
[INFO] at org.apache.cxf.attachment.AttachmentDeserializer.initializeRootMessage(AttachmentDeserializer.java:123)
[INFO] at org.apache.cxf.attachment.AttachmentDeserializer.initializeAttachments(AttachmentDeserializer.java:92)
[INFO] at org.apache.cxf.interceptor.AttachmentInInterceptor.handleMessage(AttachmentInInterceptor.java:58)
[INFO] ... 58 more
So, I'm pretty sure the code I'm using to test, in the main-function, doesn't even get past the first out.writeBytes("--" + boundary + "\n");
, as the error states that the server code can't find the boundary anywhere. I think so because of the GrepCode code I walked through - it's at the initializing of the message, somewhere in the AttachmentInInterceptor, that the code crashes; so he doesn't even process anything of the actually written data into the stream. (Or: the server never reaches my own server code, which checks for the two attachments, etcetera).
DataOutputStream out = new DataOutputStream(urlConnection.getOutputStream());
out.writeBytes("--" + boundary + "\n");
writeJson("ad9821e6-d308-472b-b21f-308b08c3fa70", out, boundary);
writeImage("image/jpeg", "K:\\Downloads\\test.jpg", out, boundary);
out.flush();
out.close();
Anyone out here able to help me out? What am I doing wrong? Probably it's just a detail I'm writing wrong onto the urlConnection, which causes the other end of the wire not to be able to find the boundary. But I've been looking way too long at this and can't find it.
Edit: So, I made some changes:
I added:
private static final String CRLF = "\r\n";
private StringBuilder text = new StringBuilder();
And changed :
public void writeJson(String value) {
String s = "--" + boundary + CRLF;
s += "Content-Type: application/json" + CRLF + CRLF;
s += "{\"objectToLinkTo\":\"" + value + "\"}" + CRLF + CRLF;
text.append(s);
}
public void writeBinaryFile(String fileName, String mimeType, String encoding) throws Exception {
String s = "--" + boundary + CRLF;
s += "Content-Type: " + mimeType + CRLF;
text.append(s);
java.nio.file.Path path = Paths.get(fileName);
byte[] data = Files.readAllBytes(path);
text.append(new String(data, encoding)).append(CRLF);
}
public void finish() {
text.append("--").append(boundary).append("--").append(CRLF);
}
private static void printResponse(URLConnection urlConnection) {
try {
InputStream is = urlConnection.getInputStream();
while (is.available() != 0) {
byte[] data = new byte[is.available()];
is.read(data);
System.out.println(new String (data, "UTF-8"));
}
} catch (Exception exc) {
exc.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
PhotoTest photoTest = new PhotoTest();
photoTest.writeJson("ad9821e6-d308-472b-b21f-308b08c3fa70");
//photoTest.writeBinaryFile("K:\\Downloads\\test.jpg", "image/jpeg", "UTF-8");
photoTest.finish();
System.out.println(photoTest.getText());
URL url = new URL(MEDIA_ADD_PHOTO);
URLConnection urlConnection = url.openConnection();
urlConnection.setDoOutput(true);
urlConnection.setUseCaches(false);
((HttpURLConnection) urlConnection).setRequestMethod("POST");
urlConnection.setRequestProperty("Accept-Charset", "UTF-8");
urlConnection.setRequestProperty("Content-Type", "multipart/related; boundary=" + photoTest.getBoundary());
urlConnection.setRequestProperty("Content-Length", Integer.toString(photoTest.getLength()));
OutputStream output = urlConnection.getOutputStream();
output.write(photoTest.getContent().getBytes());
output.flush();
output.close();
printResponse(urlConnection);
}
For now, I removed the image part (just to check if I can get past the interceptors to my own server code, which should check if the image is present). As you can see I printed the message that is built to console, it gives me this:
--the_photo
Content-Type: application/json
{"objectToLinkTo":"ad9821e6-d308-472b-b21f-308b08c3fa70"}
--the_photo--
I still get the same error. Can anyone please explain me what I'm doing wrong?
Ok, so it ended up being a small mistake somewhere, as I thought.
I randomly started to edit things - I actually wanted to try to debug the whole thing but as I never reached my own server side code and got stuck in the Apache CXF layer, I couldn't. Then I thought, let's simply comment the interceptor out:
<jaxrs:server address="/v1/photos">
<jaxrs:serviceBeans>
<ref bean="resourceV1" />
</jaxrs:serviceBeans>
<!--<jaxrs:inInterceptors>-->
<!--<bean class="org.apache.cxf.interceptor.AttachmentInInterceptor"/>-->
<!--</jaxrs:inInterceptors>-->
<jaxrs:features>
<cxf:logging />
</jaxrs:features>
<jaxrs:providers>
<bean class="commons.exceptions.exceptionwrapper.BusinessExceptionWrapper" />
<bean class="commons.exceptions.exceptionwrapper.RuntimeExceptionWrapper" />
<bean class="org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider" />
</jaxrs:providers>
</jaxrs:server>
Magically: it works now. I don't know why I had the AttachmentInInterceptor
in there to start with.
Thanks for your help.