blob: 3e65de369f08351cd493626023c714cefde3a3de [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache 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.apache.org/licenses/LICENSE-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.apache.cxf.attachment;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.activation.DataHandler;
import org.apache.cxf.common.util.Base64Utility;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.message.Attachment;
import org.apache.cxf.message.Message;
public class AttachmentSerializer {
// http://tools.ietf.org/html/rfc2387
private static final String DEFAULT_MULTIPART_TYPE = "multipart/related";
private String contentTransferEncoding = AttachmentUtil.BINARY;
private Message message;
private String bodyBoundary;
private OutputStream out;
private String encoding;
private String multipartType;
private Map<String, List<String>> rootHeaders = Collections.emptyMap();
private boolean xop = true;
private boolean writeOptionalTypeParameters = true;
public AttachmentSerializer(Message messageParam) {
message = messageParam;
}
public AttachmentSerializer(Message messageParam,
String multipartType,
boolean writeOptionalTypeParameters,
Map<String, List<String>> headers) {
message = messageParam;
this.multipartType = multipartType;
this.writeOptionalTypeParameters = writeOptionalTypeParameters;
this.rootHeaders = headers;
}
/**
* Serialize the beginning of the attachment which includes the MIME
* beginning and headers for the root message.
*/
public void writeProlog() throws IOException {
// Create boundary for body
bodyBoundary = AttachmentUtil.getUniqueBoundaryValue();
String bodyCt = (String) message.get(Message.CONTENT_TYPE);
String bodyCtParams = null;
String bodyCtParamsEscaped = null;
// split the bodyCt to its head that is the type and its properties so that we
// can insert the values at the right places based on the soap version and the mtom option
// bodyCt will be of the form
// soap11 -> text/xml
// soap12 -> application/soap+xml; action="urn:ihe:iti:2007:RetrieveDocumentSet"
if (bodyCt.indexOf(';') != -1) {
int pos = bodyCt.indexOf(';');
// get everything from the semi-colon
bodyCtParams = bodyCt.substring(pos);
bodyCtParamsEscaped = escapeQuotes(bodyCtParams);
// keep the type/subtype part in bodyCt
bodyCt = bodyCt.substring(0, pos);
}
// Set transport mime type
String requestMimeType = multipartType == null ? DEFAULT_MULTIPART_TYPE : multipartType;
StringBuilder ct = new StringBuilder(32);
ct.append(requestMimeType);
// having xop set to true implies multipart/related, but just in case...
boolean xopOrMultipartRelated = xop
|| DEFAULT_MULTIPART_TYPE.equalsIgnoreCase(requestMimeType)
|| DEFAULT_MULTIPART_TYPE.startsWith(requestMimeType);
// type is a required parameter for multipart/related only
if (xopOrMultipartRelated
&& requestMimeType.indexOf("type=") == -1) {
if (xop) {
ct.append("; type=\"application/xop+xml\"");
} else {
ct.append("; type=\"").append(bodyCt).append('"');
}
}
// boundary
ct.append("; boundary=\"")
.append(bodyBoundary)
.append('"');
String rootContentId = getHeaderValue("Content-ID", AttachmentUtil.BODY_ATTACHMENT_ID);
// 'start' is a required parameter for XOP/MTOM, clearly defined
// for simpler multipart/related payloads but is not needed for
// multipart/mixed, multipart/form-data
if (xopOrMultipartRelated) {
ct.append("; start=\"<")
.append(checkAngleBrackets(rootContentId))
.append(">\"");
}
// start-info is a required parameter for XOP/MTOM, may be needed for
// other WS cases but is redundant in simpler multipart/related cases
// the parameters need to be included within the start-info's value in the escaped form
if (writeOptionalTypeParameters || xop) {
ct.append("; start-info=\"")
.append(bodyCt);
if (bodyCtParamsEscaped != null) {
ct.append(bodyCtParamsEscaped);
}
ct.append('"');
}
message.put(Message.CONTENT_TYPE, ct.toString());
// 2. write headers
out = message.getContent(OutputStream.class);
encoding = (String) message.get(Message.ENCODING);
if (encoding == null) {
encoding = StandardCharsets.UTF_8.name();
}
StringWriter writer = new StringWriter();
writer.write("\r\n");
writer.write("--");
writer.write(bodyBoundary);
StringBuilder mimeBodyCt = new StringBuilder();
String bodyType = getHeaderValue("Content-Type", null);
if (bodyType == null) {
mimeBodyCt.append(xop ? "application/xop+xml" : bodyCt)
.append("; charset=").append(encoding);
if (xop) {
mimeBodyCt.append("; type=\"").append(bodyCt);
if (bodyCtParamsEscaped != null) {
mimeBodyCt.append(bodyCtParamsEscaped);
}
mimeBodyCt.append('"');
} else if (bodyCtParams != null) {
mimeBodyCt.append(bodyCtParams);
}
} else {
mimeBodyCt.append(bodyType);
}
writeHeaders(mimeBodyCt.toString(), rootContentId, rootHeaders, writer);
out.write(writer.getBuffer().toString().getBytes(encoding));
}
private static String escapeQuotes(String s) {
return s.indexOf('"') != 0 ? s.replace("\"", "\\\"") : s;
}
public void setContentTransferEncoding(String cte) {
contentTransferEncoding = cte;
}
private String getHeaderValue(String name, String defaultValue) {
List<String> value = rootHeaders.get(name);
if (value == null || value.isEmpty()) {
return defaultValue;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < value.size(); i++) {
sb.append(value.get(i));
if (i + 1 < value.size()) {
sb.append(',');
}
}
return sb.toString();
}
private void writeHeaders(String contentType, String attachmentId,
Map<String, List<String>> headers, Writer writer) throws IOException {
writer.write("\r\nContent-Type: ");
writer.write(contentType);
writer.write("\r\nContent-Transfer-Encoding: " + contentTransferEncoding + "\r\n");
if (attachmentId != null) {
attachmentId = checkAngleBrackets(attachmentId);
writer.write("Content-ID: <");
writer.write(URLDecoder.decode(attachmentId, StandardCharsets.UTF_8.name()));
writer.write(">\r\n");
}
// headers like Content-Disposition need to be serialized
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String name = entry.getKey();
if ("Content-Type".equalsIgnoreCase(name) || "Content-ID".equalsIgnoreCase(name)
|| "Content-Transfer-Encoding".equalsIgnoreCase(name)) {
continue;
}
writer.write(name);
writer.write(": ");
List<String> values = entry.getValue();
for (int i = 0; i < values.size(); i++) {
writer.write(values.get(i));
if (i + 1 < values.size()) {
writer.write(",");
}
}
writer.write("\r\n");
}
writer.write("\r\n");
}
private static String checkAngleBrackets(String value) {
if (value.charAt(0) == '<' && value.charAt(value.length() - 1) == '>') {
return value.substring(1, value.length() - 1);
}
return value;
}
/**
* Write the end of the body boundary and any attachments included.
* @throws IOException
*/
public void writeAttachments() throws IOException {
if (message.getAttachments() != null) {
for (Attachment a : message.getAttachments()) {
StringWriter writer = new StringWriter();
writer.write("\r\n--");
writer.write(bodyBoundary);
final Map<String, List<String>> headers;
Iterator<String> it = a.getHeaderNames();
if (it.hasNext()) {
headers = new LinkedHashMap<>();
while (it.hasNext()) {
String key = it.next();
headers.put(key, Collections.singletonList(a.getHeader(key)));
}
} else {
headers = Collections.emptyMap();
}
DataHandler handler = a.getDataHandler();
handler.setCommandMap(AttachmentUtil.getCommandMap());
writeHeaders(handler.getContentType(), a.getId(),
headers, writer);
out.write(writer.getBuffer().toString().getBytes(encoding));
if ("base64".equals(contentTransferEncoding)) {
try (InputStream inputStream = handler.getInputStream()) {
encodeBase64(inputStream, out, IOUtils.DEFAULT_BUFFER_SIZE);
}
} else {
handler.writeTo(out);
}
}
}
StringWriter writer = new StringWriter();
writer.write("\r\n--");
writer.write(bodyBoundary);
writer.write("--");
out.write(writer.getBuffer().toString().getBytes(encoding));
out.flush();
}
private int encodeBase64(InputStream input, OutputStream output, int bufferSize) throws IOException {
int avail = input.available();
if (avail > 262143) {
//must be divisible by 3
avail = 262143;
}
if (avail > bufferSize) {
bufferSize = avail;
}
final byte[] buffer = new byte[bufferSize];
int n = input.read(buffer);
int total = 0;
while (-1 != n) {
if (n == 0) {
throw new IOException("0 bytes read in violation of InputStream.read(byte[])");
}
//make sure n is divisible by 3
int left = n % 3;
n -= left;
if (n > 0) {
Base64Utility.encodeAndStream(buffer, 0, n, output);
total += n;
}
if (left != 0) {
for (int x = 0; x < left; ++x) {
buffer[x] = buffer[n + x];
}
n = input.read(buffer, left, buffer.length - left);
if (n == -1) {
// we've hit the end, but still have stuff left, write it out
Base64Utility.encodeAndStream(buffer, 0, left, output);
total += left;
}
} else {
n = input.read(buffer);
}
}
return total;
}
public boolean isXop() {
return xop;
}
public void setXop(boolean xop) {
this.xop = xop;
}
}