/**
* Copyright (C) 2010 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.xforms.submission;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.entity.mime.content.StringBody;
import org.orbeon.dom.Document;
import org.orbeon.dom.Element;
import org.orbeon.dom.QName;
import org.orbeon.dom.VisitorSupport;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.resources.URLFactory;
import org.orbeon.oxf.util.NetUtils;
import org.orbeon.oxf.util.StringUtils;
import org.orbeon.oxf.xforms.XFormsContainingDocument;
import org.orbeon.oxf.xforms.control.XFormsControl;
import org.orbeon.oxf.xforms.control.controls.XFormsUploadControl;
import org.orbeon.oxf.xforms.model.InstanceData;
import org.orbeon.oxf.xforms.model.XFormsInstance;
import org.orbeon.oxf.xml.XMLConstants;
import org.orbeon.saxon.om.Item;
import org.orbeon.saxon.om.NodeInfo;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.List;
/**
* Utilities for XForms submission processing.
*/
public class XFormsSubmissionUtils {
/**
* Create an application/x-www-form-urlencoded string, encoded in UTF-8, based on the elements and text content
* present in an XML document.
*
* @param document document to analyze
* @param separator separator character
* @return application/x-www-form-urlencoded string
*/
public static String createWwwFormUrlEncoded(final Document document, final String separator) {
final StringBuilder sb = new StringBuilder(100);
document.accept(new VisitorSupport() {
public final void visit(Element element) {
// We only care about elements
final List children = element.elements();
if (children == null || children.size() == 0) {
// Only consider leaves
final String text = element.getText();
if (text != null && text.length() > 0) {
// Got one!
final String localName = element.getName();
if (sb.length() > 0)
sb.append(separator);
try {
sb.append(URLEncoder.encode(localName, "UTF-8"));
sb.append('=');
sb.append(URLEncoder.encode(text, "UTF-8"));
// TODO: check if line breaks will be correcly encoded as "%0D%0A"
} catch (UnsupportedEncodingException e) {
// Should not happen: UTF-8 must be supported
throw new OXFException(e);
}
}
}
}
});
return sb.toString();
}
/**
* Implement support for XForms 1.1 section "11.9.7 Serialization as multipart/form-data".
*
* @param document XML document to submit
* @return MultipartRequestEntity
*/
public static MultipartEntity createMultipartFormData(final Document document) throws IOException {
// Visit document
final MultipartEntity multipartEntity = new MultipartEntity();
document.accept(new VisitorSupport() {
public final void visit(Element element) {
try {
// Only care about elements
// Only consider leaves i.e. elements without children elements
final List children = element.elements();
if (children == null || children.size() == 0) {
final String value = element.getText();
{
// Got one!
final String localName = element.getName();
final QName nodeType = InstanceData.getType(element);
if (XMLConstants.XS_ANYURI_QNAME.equals(nodeType)) {
// Interpret value as xs:anyURI
if (InstanceData.getValid(element) && StringUtils.trimAllToEmpty(value).length() > 0) {
// Value is valid as per xs:anyURI
// Don't close the stream here, as it will get read later when the MultipartEntity
// we create here is written to an output stream
addPart(multipartEntity, URLFactory.createURL(value).openStream(), element, value);
} else {
// Value is invalid as per xs:anyURI
// Just use the value as is (could also ignore it)
multipartEntity.addPart(localName, new StringBody(value, Charset.forName("UTF-8")));
}
} else if (XMLConstants.XS_BASE64BINARY_QNAME.equals(nodeType)) {
// Interpret value as xs:base64Binary
if (InstanceData.getValid(element) && StringUtils.trimAllToEmpty(value).length() > 0) {
// Value is valid as per xs:base64Binary
addPart(multipartEntity, new ByteArrayInputStream(NetUtils.base64StringToByteArray(value)), element, null);
} else {
// Value is invalid as per xs:base64Binary
// Just use the value as is (could also ignore it)
multipartEntity.addPart(localName, new StringBody(value, Charset.forName("UTF-8")));
}
} else {
// Just use the value as is
multipartEntity.addPart(localName, new StringBody(value, Charset.forName("UTF-8")));
}
}
}
} catch (IOException e) {
throw new OXFException(e);
}
}
});
return multipartEntity;
}
static private void addPart(MultipartEntity multipartEntity, InputStream inputStream, Element element, String url) {
// Gather mediatype and filename if known
// NOTE: special MIP-like annotations were added just before re-rooting/pruning element. Those will be
// removed during the next recalculate.
// See this WG action item (which was decided but not carried out): "Clarify that upload activation produces
// content and possibly filename and mediatype info as metadata. If available, filename and mediatype are copied
// to instance data if upload filename and mediatype elements are specified. At serialization, filename and
// mediatype from instance data are used if upload filename and mediatype are specified; otherwise, filename and
// mediatype are drawn from upload metadata, if they were available at time of upload activation"
//
// See:
// http://lists.w3.org/Archives/Public/public-forms/2009May/0052.html
// http://lists.w3.org/Archives/Public/public-forms/2009Apr/att-0010/2009-04-22.html#ACTION2
//
// See also this clarification:
// http://lists.w3.org/Archives/Public/public-forms/2009May/0053.html
// http://lists.w3.org/Archives/Public/public-forms/2009Apr/att-0003/2009-04-01.html#ACTION1
//
// The bottom line is that if we can find the xf:upload control bound to a node to submit, we try to get
// metadata from that control. If that fails (which can be because the control is non-relevant, bound to another
// control, or never had nested xf:filename/xf:mediatype elements), we try URL metadata. URL metadata is only
// present on nodes written by xf:upload as temporary file: URLs. It is not present if the data is stored as
// xs:base64Binary. In any case, metadata can be absent.
//
// If an xf:upload control saved data to a node as xs:anyURI, has xf:filename/xf:mediatype elements, is still
// relevant and bound to the original node (as well as its children elements), and if the nodes pointed to by
// the children elements have not been modified (e.g. by xf:setvalue), then retrieving the metadata via
// xf:upload should be equivalent to retrieving it via the URL metadata.
//
// Benefits of URL metadata: a single xf:upload can be used to save data to multiple nodes over time, and it
// doesn't have to be relevant and bound upon submission.
//
// Benefits of using xf:upload metadata: it is possible to modify the filename and mediatype subsequently.
//
// URL metadata was added 2012-05-29.
// Get mediatype, first via xf:upload control, or, if not found, try URL metadata
String mediatype = InstanceData.getTransientAnnotation(element, "xxforms-mediatype");
if (mediatype == null && url != null)
mediatype = XFormsUploadControl.getParameterOrNull(url, "mediatype");
// Get filename, first via xf:upload control, or, if not found, try URL metadata
String filename = InstanceData.getTransientAnnotation(element, "xxforms-filename");
if (filename == null && url != null)
filename = XFormsUploadControl.getParameterOrNull(url, "filename");
final ContentBody contentBody = new InputStreamBody(inputStream, mediatype, filename);
multipartEntity.addPart(element.getName(), contentBody);
}
/**
* Annotate the DOM with information about file name and mediatype provided by uploads if available.
*
* @param containingDocument current XFormsContainingDocument
* @param currentInstance instance containing the nodes to check
*/
public static void annotateBoundRelevantUploadControls(XFormsContainingDocument containingDocument, XFormsInstance currentInstance) {
for (XFormsControl currentControl : containingDocument.getControls().getCurrentControlTree().getUploadControlsJava()) {
if (currentControl.isRelevant()) {
final XFormsUploadControl currentUploadControl = (XFormsUploadControl) currentControl;
final Item controlBoundItem = currentUploadControl.getBoundItem();
if (controlBoundItem instanceof NodeInfo) {
final NodeInfo controlBoundNodeInfo = (NodeInfo) controlBoundItem;
if (currentInstance == currentInstance.model().getInstanceForNode(controlBoundNodeInfo)) {
// Found one relevant upload control bound to the instance we are submitting
// NOTE: special MIP-like annotations were added just before re-rooting/pruning element. Those
// will be removed during the next recalculate.
final String fileName = currentUploadControl.boundFilename();
if (fileName != null) {
InstanceData.setTransientAnnotation(controlBoundNodeInfo, "xxforms-filename", fileName);
}
final String mediatype = currentUploadControl.boundFileMediatype();
if (mediatype != null) {
InstanceData.setTransientAnnotation(controlBoundNodeInfo, "xxforms-mediatype", mediatype);
}
}
}
}
}
}
}