diff options
Diffstat (limited to 'src/com/kenai/jbosh/ComposableBody.java')
-rw-r--r-- | src/com/kenai/jbosh/ComposableBody.java | 345 |
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("'", "'"); + } + + /** + * 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(); + } + +} |