aboutsummaryrefslogtreecommitdiff
path: root/src/com/kenai/jbosh/ComposableBody.java
blob: d37547827f0d118be75dac5960f77101b5b44538 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
/*
 * 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("'", "&apos;");
    }

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

}