blob: d37547827f0d118be75dac5960f77101b5b44538 [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/*
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
17package com.kenai.jbosh;
18
19import java.util.Collections;
20import java.util.HashMap;
21import java.util.Map;
22import java.util.concurrent.atomic.AtomicReference;
23import java.util.regex.Matcher;
24import java.util.regex.Pattern;
25import 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 */
54public 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("'", "&apos;");
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}