/*
* Copyright 2009 Mike Cumings
*
* Licensed 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 com.kenai.jbosh;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
/**
* Implementation of the {@code AbstractBody} class which allows for the
* definition of messages from individual elements of a body.
* <p/>
* A message is constructed by creating a builder, manipulating the
* configuration of the builder, and then building it into a class instance,
* as in the following example:
* <pre>
* ComposableBody body = ComposableBody.builder()
* .setNamespaceDefinition("foo", "http://foo.com/bar")
* .setPayloadXML("<foo:data>Data to send to remote server</foo:data>")
* .build();
* </pre>
* Class instances can also be "rebuilt", allowing them to be used as templates
* when building many similar messages:
* <pre>
* ComposableBody body2 = body.rebuild()
* .setPayloadXML("<foo:data>More data to send</foo:data>")
* .build();
* </pre>
* This class does only minimal syntactic and semantic checking with respect
* to what the generated XML will look like. It is up to the developer to
* protect against the definition of malformed XML messages when building
* instances of this class.
* <p/>
* Instances of this class are immutable and thread-safe.
*/
public final class ComposableBody extends AbstractBody {
/**
* Pattern used to identify the beginning {@code body} element of a
* BOSH message.
*/
private static final Pattern BOSH_START =
Pattern.compile("<" + "(?:(?:[^:\t\n\r >]+:)|(?:\\{[^\\}>]*?\\}))?"
+ "body" + "(?:[\t\n\r ][^>]*?)?" + "(/>|>)");
/**
* Map of all attributes to their values.
*/
private final Map<BodyQName, String> attrs;
/**
* Payload XML.
*/
private final String payload;
/**
* Computed raw XML.
*/
private final AtomicReference<String> computed =
new AtomicReference<String>();
/**
* Class instance builder, after the builder pattern. This allows each
* message instance to be immutable while providing flexibility when
* building new messages.
* <p/>
* Instances of this class are <b>not</b> thread-safe.
*/
public static final class Builder {
private Map<BodyQName, String> map;
private boolean doMapCopy;
private String payloadXML;
/**
* Prevent direct construction.
*/
private Builder() {
// Empty
}
/**
* Creates a builder which is initialized to the values of the
* provided {@code ComposableBody} instance. This allows an
* existing {@code ComposableBody} to be used as a
* template/starting point.
*
* @param source body template
* @return builder instance
*/
private static Builder fromBody(final ComposableBody source) {
Builder result = new Builder();
result.map = source.getAttributes();
result.doMapCopy = true;
result.payloadXML = source.payload;
return result;
}
/**
* Set the body message's wrapped payload content. Any previous
* content will be replaced.
*
* @param xml payload XML content
* @return builder instance
*/
public Builder setPayloadXML(final String xml) {
if (xml == null) {
throw(new IllegalArgumentException(
"payload XML argument cannot be null"));
}
payloadXML = xml;
return this;
}
/**
* Set an attribute on the message body / wrapper element.
*
* @param name qualified name of the attribute
* @param value value of the attribute
* @return builder instance
*/
public Builder setAttribute(
final BodyQName name, final String value) {
if (map == null) {
map = new HashMap<BodyQName, String>();
} else if (doMapCopy) {
map = new HashMap<BodyQName, String>(map);
doMapCopy = false;
}
if (value == null) {
map.remove(name);
} else {
map.put(name, value);
}
return this;
}
/**
* Convenience method to set a namespace definition. This would result
* in a namespace prefix definition similar to:
* {@code <body xmlns:prefix="uri"/>}
*
* @param prefix prefix to define
* @param uri namespace URI to associate with the prefix
* @return builder instance
*/
public Builder setNamespaceDefinition(
final String prefix, final String uri) {
BodyQName qname = BodyQName.createWithPrefix(
XMLConstants.XML_NS_URI, prefix,
XMLConstants.XMLNS_ATTRIBUTE);
return setAttribute(qname, uri);
}
/**
* Build the immutable object instance with the current configuration.
*
* @return composable body instance
*/
public ComposableBody build() {
if (map == null) {
map = new HashMap<BodyQName, String>();
}
if (payloadXML == null) {
payloadXML = "";
}
return new ComposableBody(map, payloadXML);
}
}
///////////////////////////////////////////////////////////////////////////
// Constructors:
/**
* Prevent direct construction. This constructor is for body messages
* which are dynamically assembled.
*/
private ComposableBody(
final Map<BodyQName, String> attrMap,
final String payloadXML) {
super();
attrs = attrMap;
payload = payloadXML;
}
/**
* Parse a static body instance into a composable instance. This is an
* expensive operation and should not be used lightly.
* <p/>
* The current implementation does not obtain the payload XML by means of
* a proper XML parser. It uses some string pattern searching to find the
* first @{code body} element and the last element's closing tag. It is
* assumed that the static body's XML is well formed, etc.. This
* implementation may change in the future.
*
* @param body static body instance to convert
* @return composable bosy instance
* @throws BOSHException
*/
static ComposableBody fromStaticBody(final StaticBody body)
throws BOSHException {
String raw = body.toXML();
Matcher matcher = BOSH_START.matcher(raw);
if (!matcher.find()) {
throw(new BOSHException(
"Could not locate 'body' element in XML. The raw XML did"
+ " not match the pattern: " + BOSH_START));
}
String payload;
if (">".equals(matcher.group(1))) {
int first = matcher.end();
int last = raw.lastIndexOf("</");
if (last < first) {
last = first;
}
payload = raw.substring(first, last);
} else {
payload = "";
}
return new ComposableBody(body.getAttributes(), payload);
}
/**
* Create a builder instance to build new instances of this class.
*
* @return AbstractBody instance
*/
public static Builder builder() {
return new Builder();
}
/**
* If this {@code ComposableBody} instance is a dynamic instance, uses this
* {@code ComposableBody} instance as a starting point, create a builder
* which can be used to create another {@code ComposableBody} instance
* based on this one. This allows a {@code ComposableBody} instance to be
* used as a template. Note that the use of the returned builder in no
* way modifies or manipulates the current {@code ComposableBody} instance.
*
* @return builder instance which can be used to build similar
* {@code ComposableBody} instances
*/
public Builder rebuild() {
return Builder.fromBody(this);
}
///////////////////////////////////////////////////////////////////////////
// Accessors:
/**
* {@inheritDoc}
*/
public Map<BodyQName, String> getAttributes() {
return Collections.unmodifiableMap(attrs);
}
/**
* {@inheritDoc}
*/
public String toXML() {
String comp = computed.get();
if (comp == null) {
comp = computeXML();
computed.set(comp);
}
return comp;
}
/**
* Get the paylaod XML in String form.
*
* @return payload XML
*/
public String getPayloadXML() {
return payload;
}
///////////////////////////////////////////////////////////////////////////
// Private methods:
/**
* Escape the value of an attribute to ensure we maintain valid
* XML syntax.
*
* @param value value to escape
* @return escaped value
*/
private String escape(final String value) {
return value.replace("'", "'");
}
/**
* Generate a String representation of the message body.
*
* @return XML string representation of the body
*/
private String computeXML() {
BodyQName bodyName = getBodyQName();
StringBuilder builder = new StringBuilder();
builder.append("<");
builder.append(bodyName.getLocalPart());
for (Map.Entry<BodyQName, String> entry : attrs.entrySet()) {
builder.append(" ");
BodyQName name = entry.getKey();
String prefix = name.getPrefix();
if (prefix != null && prefix.length() > 0) {
builder.append(prefix);
builder.append(":");
}
builder.append(name.getLocalPart());
builder.append("='");
builder.append(escape(entry.getValue()));
builder.append("'");
}
builder.append(" ");
builder.append(XMLConstants.XMLNS_ATTRIBUTE);
builder.append("='");
builder.append(bodyName.getNamespaceURI());
builder.append("'>");
if (payload != null) {
builder.append(payload);
}
builder.append("</body>");
return builder.toString();
}
}