/* * 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(); } }