• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
Builder()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          */
fromBody(final ComposableBody source)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          */
setPayloadXML(final String xml)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          */
setAttribute( final BodyQName name, final String value)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          */
setNamespaceDefinition( final String prefix, final String uri)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          */
build()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      */
ComposableBody( final Map<BodyQName, String> attrMap, final String payloadXML)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      */
fromStaticBody(final StaticBody body)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      */
builder()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      */
rebuild()261     public Builder rebuild() {
262         return Builder.fromBody(this);
263     }
264 
265     ///////////////////////////////////////////////////////////////////////////
266     // Accessors:
267 
268     /**
269      * {@inheritDoc}
270      */
getAttributes()271     public Map<BodyQName, String> getAttributes() {
272         return Collections.unmodifiableMap(attrs);
273     }
274 
275     /**
276      * {@inheritDoc}
277      */
toXML()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      */
getPayloadXML()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      */
escape(final String value)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      */
computeXML()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