aboutsummaryrefslogtreecommitdiff
path: root/src/com/kenai/jbosh/ComposableBody.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/kenai/jbosh/ComposableBody.java')
-rw-r--r--src/com/kenai/jbosh/ComposableBody.java345
1 files changed, 345 insertions, 0 deletions
diff --git a/src/com/kenai/jbosh/ComposableBody.java b/src/com/kenai/jbosh/ComposableBody.java
new file mode 100644
index 0000000..d375478
--- /dev/null
+++ b/src/com/kenai/jbosh/ComposableBody.java
@@ -0,0 +1,345 @@
+/*
+ * 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("'", "&apos;");
+ }
+
+ /**
+ * 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();
+ }
+
+}