1 /* 2 * Copyright 2009 Mike Cumings 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.kenai.jbosh; 18 19 import java.util.Collections; 20 import java.util.HashMap; 21 import java.util.Map; 22 import java.util.concurrent.atomic.AtomicReference; 23 import java.util.regex.Matcher; 24 import java.util.regex.Pattern; 25 import javax.xml.XMLConstants; 26 27 /** 28 * Implementation of the {@code AbstractBody} class which allows for the 29 * definition of messages from individual elements of a body. 30 * <p/> 31 * A message is constructed by creating a builder, manipulating the 32 * configuration of the builder, and then building it into a class instance, 33 * as in the following example: 34 * <pre> 35 * ComposableBody body = ComposableBody.builder() 36 * .setNamespaceDefinition("foo", "http://foo.com/bar") 37 * .setPayloadXML("<foo:data>Data to send to remote server</foo:data>") 38 * .build(); 39 * </pre> 40 * Class instances can also be "rebuilt", allowing them to be used as templates 41 * when building many similar messages: 42 * <pre> 43 * ComposableBody body2 = body.rebuild() 44 * .setPayloadXML("<foo:data>More data to send</foo:data>") 45 * .build(); 46 * </pre> 47 * This class does only minimal syntactic and semantic checking with respect 48 * to what the generated XML will look like. It is up to the developer to 49 * protect against the definition of malformed XML messages when building 50 * instances of this class. 51 * <p/> 52 * Instances of this class are immutable and thread-safe. 53 */ 54 public final class ComposableBody extends AbstractBody { 55 56 /** 57 * Pattern used to identify the beginning {@code body} element of a 58 * BOSH message. 59 */ 60 private static final Pattern BOSH_START = 61 Pattern.compile("<" + "(?:(?:[^:\t\n\r >]+:)|(?:\\{[^\\}>]*?}))?" 62 + "body" + "(?:[\t\n\r ][^>]*?)?" + "(/>|>)"); 63 64 /** 65 * Map of all attributes to their values. 66 */ 67 private final Map<BodyQName, String> attrs; 68 69 /** 70 * Payload XML. 71 */ 72 private final String payload; 73 74 /** 75 * Computed raw XML. 76 */ 77 private final AtomicReference<String> computed = 78 new AtomicReference<String>(); 79 80 /** 81 * Class instance builder, after the builder pattern. This allows each 82 * message instance to be immutable while providing flexibility when 83 * building new messages. 84 * <p/> 85 * Instances of this class are <b>not</b> thread-safe. 86 */ 87 public static final class Builder { 88 private Map<BodyQName, String> map; 89 private boolean doMapCopy; 90 private String payloadXML; 91 92 /** 93 * Prevent direct construction. 94 */ 95 private Builder() { 96 // Empty 97 } 98 99 /** 100 * Creates a builder which is initialized to the values of the 101 * provided {@code ComposableBody} instance. This allows an 102 * existing {@code ComposableBody} to be used as a 103 * template/starting point. 104 * 105 * @param source body template 106 * @return builder instance 107 */ 108 private static Builder fromBody(final ComposableBody source) { 109 Builder result = new Builder(); 110 result.map = source.getAttributes(); 111 result.doMapCopy = true; 112 result.payloadXML = source.payload; 113 return result; 114 } 115 116 /** 117 * Set the body message's wrapped payload content. Any previous 118 * content will be replaced. 119 * 120 * @param xml payload XML content 121 * @return builder instance 122 */ 123 public Builder setPayloadXML(final String xml) { 124 if (xml == null) { 125 throw(new IllegalArgumentException( 126 "payload XML argument cannot be null")); 127 } 128 payloadXML = xml; 129 return this; 130 } 131 132 /** 133 * Set an attribute on the message body / wrapper element. 134 * 135 * @param name qualified name of the attribute 136 * @param value value of the attribute 137 * @return builder instance 138 */ 139 public Builder setAttribute( 140 final BodyQName name, final String value) { 141 if (map == null) { 142 map = new HashMap<BodyQName, String>(); 143 } else if (doMapCopy) { 144 map = new HashMap<BodyQName, String>(map); 145 doMapCopy = false; 146 } 147 if (value == null) { 148 map.remove(name); 149 } else { 150 map.put(name, value); 151 } 152 return this; 153 } 154 155 /** 156 * Convenience method to set a namespace definition. This would result 157 * in a namespace prefix definition similar to: 158 * {@code <body xmlns:prefix="uri"/>} 159 * 160 * @param prefix prefix to define 161 * @param uri namespace URI to associate with the prefix 162 * @return builder instance 163 */ 164 public Builder setNamespaceDefinition( 165 final String prefix, final String uri) { 166 BodyQName qname = BodyQName.createWithPrefix( 167 XMLConstants.XML_NS_URI, prefix, 168 XMLConstants.XMLNS_ATTRIBUTE); 169 return setAttribute(qname, uri); 170 } 171 172 /** 173 * Build the immutable object instance with the current configuration. 174 * 175 * @return composable body instance 176 */ 177 public ComposableBody build() { 178 if (map == null) { 179 map = new HashMap<BodyQName, String>(); 180 } 181 if (payloadXML == null) { 182 payloadXML = ""; 183 } 184 return new ComposableBody(map, payloadXML); 185 } 186 } 187 188 /////////////////////////////////////////////////////////////////////////// 189 // Constructors: 190 191 /** 192 * Prevent direct construction. This constructor is for body messages 193 * which are dynamically assembled. 194 */ 195 private ComposableBody( 196 final Map<BodyQName, String> attrMap, 197 final String payloadXML) { 198 super(); 199 attrs = attrMap; 200 payload = payloadXML; 201 } 202 203 /** 204 * Parse a static body instance into a composable instance. This is an 205 * expensive operation and should not be used lightly. 206 * <p/> 207 * The current implementation does not obtain the payload XML by means of 208 * a proper XML parser. It uses some string pattern searching to find the 209 * first @{code body} element and the last element's closing tag. It is 210 * assumed that the static body's XML is well formed, etc.. This 211 * implementation may change in the future. 212 * 213 * @param body static body instance to convert 214 * @return composable bosy instance 215 * @throws BOSHException 216 */ 217 static ComposableBody fromStaticBody(final StaticBody body) 218 throws BOSHException { 219 String raw = body.toXML(); 220 Matcher matcher = BOSH_START.matcher(raw); 221 if (!matcher.find()) { 222 throw(new BOSHException( 223 "Could not locate 'body' element in XML. The raw XML did" 224 + " not match the pattern: " + BOSH_START)); 225 } 226 String payload; 227 if (">".equals(matcher.group(1))) { 228 int first = matcher.end(); 229 int last = raw.lastIndexOf("</"); 230 if (last < first) { 231 last = first; 232 } 233 payload = raw.substring(first, last); 234 } else { 235 payload = ""; 236 } 237 238 return new ComposableBody(body.getAttributes(), payload); 239 } 240 241 /** 242 * Create a builder instance to build new instances of this class. 243 * 244 * @return AbstractBody instance 245 */ 246 public static Builder builder() { 247 return new Builder(); 248 } 249 250 /** 251 * If this {@code ComposableBody} instance is a dynamic instance, uses this 252 * {@code ComposableBody} instance as a starting point, create a builder 253 * which can be used to create another {@code ComposableBody} instance 254 * based on this one. This allows a {@code ComposableBody} instance to be 255 * used as a template. Note that the use of the returned builder in no 256 * way modifies or manipulates the current {@code ComposableBody} instance. 257 * 258 * @return builder instance which can be used to build similar 259 * {@code ComposableBody} instances 260 */ 261 public Builder rebuild() { 262 return Builder.fromBody(this); 263 } 264 265 /////////////////////////////////////////////////////////////////////////// 266 // Accessors: 267 268 /** 269 * {@inheritDoc} 270 */ 271 public Map<BodyQName, String> getAttributes() { 272 return Collections.unmodifiableMap(attrs); 273 } 274 275 /** 276 * {@inheritDoc} 277 */ 278 public String toXML() { 279 String comp = computed.get(); 280 if (comp == null) { 281 comp = computeXML(); 282 computed.set(comp); 283 } 284 return comp; 285 } 286 287 /** 288 * Get the paylaod XML in String form. 289 * 290 * @return payload XML 291 */ 292 public String getPayloadXML() { 293 return payload; 294 } 295 296 /////////////////////////////////////////////////////////////////////////// 297 // Private methods: 298 299 /** 300 * Escape the value of an attribute to ensure we maintain valid 301 * XML syntax. 302 * 303 * @param value value to escape 304 * @return escaped value 305 */ 306 private String escape(final String value) { 307 return value.replace("'", "'"); 308 } 309 310 /** 311 * Generate a String representation of the message body. 312 * 313 * @return XML string representation of the body 314 */ 315 private String computeXML() { 316 BodyQName bodyName = getBodyQName(); 317 StringBuilder builder = new StringBuilder(); 318 builder.append("<"); 319 builder.append(bodyName.getLocalPart()); 320 for (Map.Entry<BodyQName, String> entry : attrs.entrySet()) { 321 builder.append(" "); 322 BodyQName name = entry.getKey(); 323 String prefix = name.getPrefix(); 324 if (prefix != null && prefix.length() > 0) { 325 builder.append(prefix); 326 builder.append(":"); 327 } 328 builder.append(name.getLocalPart()); 329 builder.append("='"); 330 builder.append(escape(entry.getValue())); 331 builder.append("'"); 332 } 333 builder.append(" "); 334 builder.append(XMLConstants.XMLNS_ATTRIBUTE); 335 builder.append("='"); 336 builder.append(bodyName.getNamespaceURI()); 337 builder.append("'>"); 338 if (payload != null) { 339 builder.append(payload); 340 } 341 builder.append("</body>"); 342 return builder.toString(); 343 } 344 345 } 346