/* * 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. *

* 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: *

 * ComposableBody body = ComposableBody.builder()
 *     .setNamespaceDefinition("foo", "http://foo.com/bar")
 *     .setPayloadXML("Data to send to remote server")
 *     .build();
 * 
* Class instances can also be "rebuilt", allowing them to be used as templates * when building many similar messages: *
 * ComposableBody body2 = body.rebuild()
 *     .setPayloadXML("More data to send")
 *     .build();
 * 
* 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. *

* 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 attrs; /** * Payload XML. */ private final String payload; /** * Computed raw XML. */ private final AtomicReference computed = new AtomicReference(); /** * Class instance builder, after the builder pattern. This allows each * message instance to be immutable while providing flexibility when * building new messages. *

* Instances of this class are not thread-safe. */ public static final class Builder { private Map 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(); } else if (doMapCopy) { map = new HashMap(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 } * * @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(); } 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 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. *

* 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(" 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 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(""); return builder.toString(); } }